diff --git a/class/wallet-import.js b/class/wallet-import.js index fe2b9e1e7..3ad93a518 100644 --- a/class/wallet-import.js +++ b/class/wallet-import.js @@ -116,9 +116,7 @@ function WalletImport() { password = await prompt(loc.wallets.looks_like_bip38, loc.wallets.enter_bip38_password, false); } while (!password); - const decryptedKey = await bip38.decrypt(importText, password, status => { - console.warn(status.percent + '%'); - }); + const decryptedKey = await bip38.decrypt(importText, password); if (decryptedKey) { importText = wif.encode(0x80, decryptedKey.privateKey, decryptedKey.compressed); @@ -162,11 +160,33 @@ function WalletImport() { const hd4 = new HDSegwitBech32Wallet(); hd4.setSecret(importText); if (hd4.validateMnemonic()) { - await hd4.fetchBalance(); - if (hd4.getBalance() > 0) { - // await hd4.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later + // OK its a valid BIP39 seed + + if (await hd4.wasEverUsed()) { + await hd4.fetchBalance(); // fetching balance for BIP84 only on purpose return WalletImport._saveWallet(hd4); } + + const hd2 = new HDSegwitP2SHWallet(); + hd2.setSecret(importText); + if (await hd2.wasEverUsed()) { + return WalletImport._saveWallet(hd2); + } + + const hd3 = new HDLegacyP2PKHWallet(); + hd3.setSecret(importText); + if (await hd3.wasEverUsed()) { + return WalletImport._saveWallet(hd3); + } + + const hd1 = new HDLegacyBreadwalletWallet(); + hd1.setSecret(importText); + if (await hd1.wasEverUsed()) { + return WalletImport._saveWallet(hd1); + } + + // no scheme (BIP84/BIP49/BIP44/Bread) was ever used. lets import as default BIP84: + return WalletImport._saveWallet(hd4); } const segwitWallet = new SegwitP2SHWallet(); @@ -174,28 +194,24 @@ function WalletImport() { if (segwitWallet.getAddress()) { // ok its a valid WIF - const legacyWallet = new LegacyWallet(); - legacyWallet.setSecret(importText); - const segwitBech32Wallet = new SegwitBech32Wallet(); segwitBech32Wallet.setSecret(importText); - - await legacyWallet.fetchBalance(); - await segwitBech32Wallet.fetchBalance(); - if (legacyWallet.getBalance() > 0) { - // yep, its legacy we're importing - await legacyWallet.fetchTransactions(); - return WalletImport._saveWallet(legacyWallet); - } else if (segwitBech32Wallet.getBalance() > 0) { + if (await segwitBech32Wallet.wasEverUsed()) { // yep, its single-address bech32 wallet - await segwitBech32Wallet.fetchTransactions(); + await segwitBech32Wallet.fetchBalance(); return WalletImport._saveWallet(segwitBech32Wallet); - } else { - // by default, we import wif as Segwit P2SH + } + + if (await segwitWallet.wasEverUsed()) { + // yep, its single-address bech32 wallet await segwitWallet.fetchBalance(); - await segwitWallet.fetchTransactions(); return WalletImport._saveWallet(segwitWallet); } + + // default wallet is Legacy + const legacyWallet = new LegacyWallet(); + legacyWallet.setSecret(importText); + return WalletImport._saveWallet(legacyWallet); } // case - WIF is valid, just has uncompressed pubkey @@ -210,20 +226,10 @@ function WalletImport() { // if we're here - nope, its not a valid WIF - const hd1 = new HDLegacyBreadwalletWallet(); - hd1.setSecret(importText); - if (hd1.validateMnemonic()) { - await hd1.fetchBalance(); - if (hd1.getBalance() > 0) { - // await hd1.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later - return WalletImport._saveWallet(hd1); - } - } - try { const hdElectrumSeedLegacy = new HDSegwitElectrumSeedP2WPKHWallet(); hdElectrumSeedLegacy.setSecret(importText); - if (await hdElectrumSeedLegacy.wasEverUsed()) { + if (hdElectrumSeedLegacy.validateMnemonic()) { // not fetching txs or balances, fuck it, yolo, life is too short return WalletImport._saveWallet(hdElectrumSeedLegacy); } @@ -232,7 +238,7 @@ function WalletImport() { try { const hdElectrumSeedLegacy = new HDLegacyElectrumSeedP2PKHWallet(); hdElectrumSeedLegacy.setSecret(importText); - if (await hdElectrumSeedLegacy.wasEverUsed()) { + if (hdElectrumSeedLegacy.validateMnemonic()) { // not fetching txs or balances, fuck it, yolo, life is too short return WalletImport._saveWallet(hdElectrumSeedLegacy); } @@ -260,64 +266,11 @@ function WalletImport() { } } catch (_) {} - const hd2 = new HDSegwitP2SHWallet(); - hd2.setSecret(importText); - if (hd2.validateMnemonic()) { - await hd2.fetchBalance(); - if (hd2.getBalance() > 0) { - // await hd2.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later - return WalletImport._saveWallet(hd2); - } - } - - const hd3 = new HDLegacyP2PKHWallet(); - hd3.setSecret(importText); - if (hd3.validateMnemonic()) { - await hd3.fetchBalance(); - if (hd3.getBalance() > 0) { - // await hd3.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later - return WalletImport._saveWallet(hd3); - } - } - - // no balances? how about transactions count? - - if (hd1.validateMnemonic()) { - await hd1.fetchTransactions(); - if (hd1.getTransactions().length !== 0) { - return WalletImport._saveWallet(hd1); - } - } - if (hd2.validateMnemonic()) { - await hd2.fetchTransactions(); - if (hd2.getTransactions().length !== 0) { - return WalletImport._saveWallet(hd2); - } - } - if (hd3.validateMnemonic()) { - await hd3.fetchTransactions(); - if (hd3.getTransactions().length !== 0) { - return WalletImport._saveWallet(hd3); - } - } - if (hd4.validateMnemonic()) { - await hd4.fetchTransactions(); - if (hd4.getTransactions().length !== 0) { - return WalletImport._saveWallet(hd4); - } - } - - // is it even valid? if yes we will import as: - if (hd4.validateMnemonic()) { - return WalletImport._saveWallet(hd4); - } - // not valid? maybe its a watch-only address? const watchOnly = new WatchOnlyWallet(); watchOnly.setSecret(importText); if (watchOnly.valid()) { - // await watchOnly.fetchTransactions(); // experiment: dont fetch tx now. it will import faster. user can refresh his wallet later await watchOnly.fetchBalance(); return WalletImport._saveWallet(watchOnly, additionalProperties); } diff --git a/class/wallets/legacy-wallet.js b/class/wallets/legacy-wallet.js index d4beacbc7..538f5ca52 100644 --- a/class/wallets/legacy-wallet.js +++ b/class/wallets/legacy-wallet.js @@ -534,4 +534,14 @@ export class LegacyWallet extends AbstractWallet { // null, true so it can verify Electrum signatores without errors return bitcoinMessage.verify(message, address, signature, null, true); } + + /** + * Probes address for transactions, if there are any returns TRUE + * + * @returns {Promise} + */ + async wasEverUsed() { + const txs = await BlueElectrum.getTransactionsByAddress(this.getAddress()); + return txs.length > 0; + } } diff --git a/tests/integration/import.test.js b/tests/integration/import.test.js new file mode 100644 index 000000000..1c71cc144 --- /dev/null +++ b/tests/integration/import.test.js @@ -0,0 +1,180 @@ +import { + HDSegwitElectrumSeedP2WPKHWallet, + HDLegacyBreadwalletWallet, + HDSegwitBech32Wallet, + HDLegacyElectrumSeedP2PKHWallet, + LegacyWallet, + SegwitP2SHWallet, + SegwitBech32Wallet, + HDLegacyP2PKHWallet, + HDSegwitP2SHWallet, + WatchOnlyWallet, + HDAezeedWallet, +} from '../../class'; +import WalletImport from '../../class/wallet-import'; +import React from 'react'; +const assert = require('assert'); +global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js +global.tls = require('tls'); // needed by Electrum client. For RN it is proviced in shim.js +const BlueElectrum = require('../../blue_modules/BlueElectrum'); // so it connects ASAP + +/** @type HDSegwitBech32Wallet */ +let lastImportedWallet; + +React.useContext = jest.fn(() => { + return { + wallets: [], + pendingWallets: [], + setPendingWallets: function () {}, + saveToDisk: function () {}, + addWallet: function (wallet) { + lastImportedWallet = wallet; + }, + }; +}); + +jest.mock('../../blue_modules/prompt', () => { + return jest.fn(() => { + return 'qwerty'; + }); +}); + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; + +afterAll(async () => { + // after all tests we close socket so the test suite can actually terminate + BlueElectrum.forceDisconnect(); +}); + +beforeAll(async () => { + // awaiting for Electrum to be connected. For RN Electrum would naturally connect + // while app starts up, but for tests we need to wait for it + await BlueElectrum.waitTillConnected(); + WalletImport(); // wut +}); + +describe('import procedure', function () { + it('can import BIP84', async () => { + await WalletImport.processImportText( + 'always direct find escape liar turn differ shy tool gap elder galaxy lawn wild movie fog moon spread casual inner box diagram outdoor tell', + ); + assert.strictEqual(lastImportedWallet.type, HDSegwitBech32Wallet.type); + assert.strictEqual(lastImportedWallet._getExternalAddressByIndex(0), 'bc1qth9qxvwvdthqmkl6x586ukkq8zvumd38nxr08l'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported HD SegWit (BIP84 Bech32 Native)'); + }); + + it('can import Legacy', async () => { + await WalletImport.processImportText('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv'); + assert.strictEqual(lastImportedWallet.type, LegacyWallet.type); + assert.strictEqual(lastImportedWallet.getAddress(), '1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported Legacy (P2PKH)'); + }); + + it('can import Legacy P2SH Segwit', async () => { + await WalletImport.processImportText('L3NxFnYoBGjJ5PhxrxV6jorvjnc8cerYJx71vXU6ta8BXQxHVZya'); + assert.strictEqual(lastImportedWallet.type, SegwitP2SHWallet.type); + assert.strictEqual(lastImportedWallet.getAddress(), '3KM9VfdsDf9uT7uwZagoKgVn8z35m9CtSM'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported SegWit (P2SH)'); + }); + + // todo: + it('can import Legacy Bech32 Segwit', async () => { + await WalletImport.processImportText('L1T6FfKpKHi8JE6eBKrsXkenw34d5FfFzJUZ6dLs2utxkSvsDfxZ'); + assert.strictEqual(lastImportedWallet.type, SegwitBech32Wallet.type); + assert.strictEqual(lastImportedWallet.getAddress(), 'bc1q763rf54hzuncmf8dtlz558uqe4f247mq39rjvr'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported P2 WPKH'); + }); + + it('can import BIP44', async () => { + await WalletImport.processImportText( + 'sting museum endless duty nice riot because swallow brother depth weapon merge woman wish hold finish venture gauge stomach bomb device bracket agent parent', + ); + assert.strictEqual(lastImportedWallet.type, HDLegacyP2PKHWallet.type); + assert.strictEqual(lastImportedWallet._getExternalAddressByIndex(0), '1EgDbwf5nXp9knoaWW6nV6N91EK3EFQ5vC'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported HD Legacy (BIP44 P2PKH)'); + }); + + it('can import BIP49', async () => { + await WalletImport.processImportText( + 'believe torch sport lizard absurd retreat scale layer song pen clump combine window staff dream filter latin bicycle vapor anchor put clean gain slush', + ); + assert.strictEqual(lastImportedWallet.type, HDSegwitP2SHWallet.type); + assert.strictEqual(lastImportedWallet._getExternalAddressByIndex(0), '3EoqYYp7hQSHn5nHqRtWzkgqmK3caQ2SUu'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported HD SegWit (BIP49 P2SH)'); + }); + + it('can import BIP49', async () => { + await WalletImport.processImportText( + 'believe torch sport lizard absurd retreat scale layer song pen clump combine window staff dream filter latin bicycle vapor anchor put clean gain slush', + ); + assert.strictEqual(lastImportedWallet.type, HDSegwitP2SHWallet.type); + assert.strictEqual(lastImportedWallet._getExternalAddressByIndex(0), '3EoqYYp7hQSHn5nHqRtWzkgqmK3caQ2SUu'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported HD SegWit (BIP49 P2SH)'); + }); + + it('can import HD Legacy Electrum (BIP32 P2PKH)', async () => { + await WalletImport.processImportText('eight derive blast guide smoke piece coral burden lottery flower tomato flame'); + assert.strictEqual(lastImportedWallet.type, HDLegacyElectrumSeedP2PKHWallet.type); + assert.strictEqual(lastImportedWallet._getExternalAddressByIndex(0), '1FgVfJ5D3HyKWKC4xk36Cio7MUaxxnXaVd'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported HD Legacy Electrum (BIP32 P2PKH)'); + }); + + it('can import BreadWallet', async () => { + await WalletImport.processImportText( + 'tired lesson alert attend giggle fancy nose enter ethics fashion fly dove dutch hidden toe argue save fish catch patient waste gift divorce whisper', + ); + assert.strictEqual(lastImportedWallet.type, HDLegacyBreadwalletWallet.type); + assert.strictEqual(lastImportedWallet._getExternalAddressByIndex(0), '1j7TbbTv8adcZFr4RC7Cyr7GN9VGYTecu'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported HD Legacy Breadwallet (P2PKH)'); + }); + + it('can import HD Electrum (BIP32 P2WPKH)', async () => { + await WalletImport.processImportText('noble mimic pipe merry knife screen enter dune crop bonus slice card'); + assert.strictEqual(lastImportedWallet.type, HDSegwitElectrumSeedP2WPKHWallet.type); + assert.strictEqual(lastImportedWallet._getExternalAddressByIndex(0), 'bc1qzzanxnr3xv9a5ha264kpzpfq260qvuameslddu'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported HD Electrum (BIP32 P2WPKH)'); + }); + + it('can import AEZEED', async () => { + await WalletImport.processImportText( + 'abstract rhythm weird food attract treat mosquito sight royal actor surround ride strike remove guilt catch filter summer mushroom protect poverty cruel chaos pattern', + ); + assert.strictEqual(lastImportedWallet.type, HDAezeedWallet.type); + }); + + it('importing empty BIP39 should yield BIP84', async () => { + const tempWallet = new HDSegwitBech32Wallet(); + await tempWallet.generate(); + await WalletImport.processImportText(tempWallet.getSecret()); + assert.strictEqual(lastImportedWallet.type, HDSegwitBech32Wallet.type); + }); + + it('can import Legacy with uncompressed pubkey', async () => { + await WalletImport.processImportText('5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS'); + assert.strictEqual(lastImportedWallet.getSecret(), '5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS'); + assert.strictEqual(lastImportedWallet.type, LegacyWallet.type); + assert.strictEqual(lastImportedWallet.getAddress(), '1GsJDeD6fqS912egpjhdjrUTiCh1hhwBgQ'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported Legacy (P2PKH)'); + }); + + it('can import BIP38 encrypted backup', async () => { + await WalletImport.processImportText('6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN'); + assert.strictEqual(lastImportedWallet.getSecret(), 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc'); + assert.strictEqual(lastImportedWallet.type, LegacyWallet.type); + assert.strictEqual(lastImportedWallet.getAddress(), '1639W2kM6UY9PdavMQeLqG4SuUEae9NZfq'); + assert.strictEqual(lastImportedWallet.getLabel(), 'Imported Legacy (P2PKH)'); + }); + + it('can import watch-only address', async () => { + await WalletImport.processImportText('1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78'); + assert.strictEqual(lastImportedWallet.type, WatchOnlyWallet.type); + await WalletImport.processImportText('3EoqYYp7hQSHn5nHqRtWzkgqmK3caQ2SUu'); + assert.strictEqual(lastImportedWallet.type, WatchOnlyWallet.type); + await WalletImport.processImportText('bc1q8j4lk4qlhun0n7h5ahfslfldc8zhlxgynfpdj2'); + assert.strictEqual(lastImportedWallet.type, WatchOnlyWallet.type); + await WalletImport.processImportText( + 'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP', + ); + assert.strictEqual(lastImportedWallet.type, WatchOnlyWallet.type); + }); +}); diff --git a/tests/setup.js b/tests/setup.js index b0d384b04..d72a09702 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -109,3 +109,29 @@ jest.mock('realm', () => { open: jest.fn(() => realmInstanceMock), }; }); + +jest.mock('react-native-idle-timer', () => { + return { + setIdleTimerDisabled: jest.fn(), + }; +}); + +jest.mock('react-native-haptic-feedback', () => { + return { + trigger: jest.fn(), + }; +}); + +jest.mock('../blue_modules/analytics', () => { + const ret = jest.fn(); + ret.ENUM = { CREATED_WALLET: '' }; + return ret; +}); + +jest.mock('../blue_modules/notifications', () => { + return { + majorTomToGroundControl: jest.fn(), + }; +}); + +global.alert = () => {}; diff --git a/tests/unit/hd-aezeed.test.js b/tests/unit/hd-aezeed.test.js index 51af462dd..b392df2ab 100644 --- a/tests/unit/hd-aezeed.test.js +++ b/tests/unit/hd-aezeed.test.js @@ -36,6 +36,12 @@ describe('HDAezeedWallet', () => { assert.ok(await aezeed.validateMnemonicAsync()); assert.ok(!(await aezeed.mnemonicInvalidPassword())); + aezeed.setSecret( + 'able concert slush lend olive cost wagon dawn board robot park snap dignity churn fiction quote shrimp hammer wing jump immune skill sunset west:aezeed', + ); + assert.ok(await aezeed.validateMnemonicAsync()); + assert.ok(!(await aezeed.mnemonicInvalidPassword())); + aezeed.setSecret( 'abstract rhythm weird food attract treat mosquito sight royal actor surround ride strike remove guilt catch filter summer mushroom protect poverty cruel chaos pattern', ); diff --git a/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js b/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js index bf6af564a..dd0dbb61f 100644 --- a/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js +++ b/tests/unit/hd-legacy-electrum-seed-p2pkh-wallet.test.js @@ -2,6 +2,20 @@ import { HDLegacyElectrumSeedP2PKHWallet } from '../../class'; const assert = require('assert'); describe('HDLegacyElectrumSeedP2PKHWallet', () => { + it('wont accept BIP39 seed', () => { + const hd = new HDLegacyElectrumSeedP2PKHWallet(); + hd.setSecret( + 'honey risk juice trip orient galaxy win situate shoot anchor bounce remind horse traffic exotic since escape mimic ramp skin judge owner topple erode', + ); + assert.ok(!hd.validateMnemonic()); + }); + + it('wont accept electrum seed, but SEGWIT seed', () => { + const hd = new HDLegacyElectrumSeedP2PKHWallet(); + hd.setSecret('method goddess humble crumble output snake essay carpet monster barely trip betray '); + assert.ok(!hd.validateMnemonic()); + }); + it('can import mnemonics and generate addresses and WIFs', async function () { const hd = new HDLegacyElectrumSeedP2PKHWallet(); hd.setSecret('receive happy wash prosper update pet neck acid try profit proud hungry '); diff --git a/tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js b/tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js index f4a2529a6..09bb7277d 100644 --- a/tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js +++ b/tests/unit/hd-segwit-electrum-seed-p2wpkh-wallet.test.js @@ -2,6 +2,18 @@ import { HDSegwitElectrumSeedP2WPKHWallet } from '../../class'; const assert = require('assert'); describe('HDSegwitElectrumSeedP2WPKHWallet', () => { + it('wont accept BIP39 seed', () => { + const hd = new HDSegwitElectrumSeedP2WPKHWallet(); + hd.setSecret('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'); + assert.ok(!hd.validateMnemonic()); + }); + + it('wont accept electrum seed, but STANDARD p2pkh seed', () => { + const hd = new HDSegwitElectrumSeedP2WPKHWallet(); + hd.setSecret('receive happy wash prosper update pet neck acid try profit proud hungry '); + assert.ok(!hd.validateMnemonic()); + }); + it('can import mnemonics and generate addresses and WIFs', async function () { const hd = new HDSegwitElectrumSeedP2WPKHWallet(); hd.setSecret('method goddess humble crumble output snake essay carpet monster barely trip betray ');