Merge pull request #7293 from BlueWallet/import-offline

feat: offline import
This commit is contained in:
GLaDOS 2024-11-12 14:20:25 +00:00 committed by GitHub
commit 219130a5e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 230 additions and 143 deletions

View File

@ -1113,7 +1113,7 @@ export const testConnection = async function (host: string, tcpPort?: number, ss
}; };
export const forceDisconnect = (): void => { export const forceDisconnect = (): void => {
mainClient.close(); mainClient?.close();
}; };
export const setBatchingDisabled = () => { export const setBatchingDisabled = () => {

View File

@ -53,6 +53,7 @@ const startImport = (
importTextOrig: string, importTextOrig: string,
askPassphrase: boolean = false, askPassphrase: boolean = false,
searchAccounts: boolean = false, searchAccounts: boolean = false,
offline: boolean = false,
onProgress: (name: string) => void, onProgress: (name: string) => void,
onWallet: (wallet: TWallet) => void, onWallet: (wallet: TWallet) => void,
onPassword: (title: string, text: string) => Promise<string>, onPassword: (title: string, text: string) => Promise<string>,
@ -67,6 +68,18 @@ const startImport = (
promiseReject = reject; promiseReject = reject;
}); });
// helpers
// in offline mode all wallets are considered used
const wasUsed = async (wallet: TWallet): Promise<boolean> => {
if (offline) return true;
return wallet.wasEverUsed();
};
const fetch = async (wallet: TWallet, balance: boolean = false, transactions: boolean = false) => {
if (offline) return;
if (balance) await wallet.fetchBalance();
if (transactions) await wallet.fetchTransactions();
};
// actions // actions
const reportProgress = (name: string) => { const reportProgress = (name: string) => {
onProgress(name); onProgress(name);
@ -165,7 +178,7 @@ const startImport = (
const ms = new MultisigHDWallet(); const ms = new MultisigHDWallet();
ms.setSecret(text); ms.setSecret(text);
if (ms.getN() > 0 && ms.getM() > 0) { if (ms.getN() > 0 && ms.getM() > 0) {
await ms.fetchBalance(); await fetch(ms, true, false);
yield { wallet: ms }; yield { wallet: ms };
} }
@ -179,11 +192,13 @@ const startImport = (
lnd.setSecret(split[0]); lnd.setSecret(split[0]);
} }
await lnd.init(); await lnd.init();
if (!offline) {
await lnd.authorize(); await lnd.authorize();
await lnd.fetchTransactions(); await lnd.fetchTransactions();
await lnd.fetchUserInvoices(); await lnd.fetchUserInvoices();
await lnd.fetchPendingTransactions(); await lnd.fetchPendingTransactions();
await lnd.fetchBalance(); await lnd.fetchBalance();
}
yield { wallet: lnd }; yield { wallet: lnd };
} }
@ -228,7 +243,7 @@ const startImport = (
} }
wallet.setDerivationPath(path); wallet.setDerivationPath(path);
yield { progress: `bip39 ${i.script_type} ${path}` }; yield { progress: `bip39 ${i.script_type} ${path}` };
if (await wallet.wasEverUsed()) { if (await wasUsed(wallet)) {
yield { wallet }; yield { wallet };
walletFound = true; walletFound = true;
} else { } else {
@ -247,11 +262,12 @@ const startImport = (
m0Legacy.setDerivationPath("m/0'"); m0Legacy.setDerivationPath("m/0'");
yield { progress: "bip39 p2pkh m/0'" }; yield { progress: "bip39 p2pkh m/0'" };
// BRD doesn't support passphrase and only works with 12 words seeds // BRD doesn't support passphrase and only works with 12 words seeds
if (!password && text.split(' ').length === 12) { // do not try to guess BRD wallet in offline mode
if (!password && text.split(' ').length === 12 && !offline) {
const brd = new HDLegacyBreadwalletWallet(); const brd = new HDLegacyBreadwalletWallet();
brd.setSecret(text); brd.setSecret(text);
if (await m0Legacy.wasEverUsed()) { if (await wasUsed(m0Legacy)) {
await m0Legacy.fetchBalance(); await m0Legacy.fetchBalance();
await m0Legacy.fetchTransactions(); await m0Legacy.fetchTransactions();
yield { progress: 'BRD' }; yield { progress: 'BRD' };
@ -265,7 +281,7 @@ const startImport = (
walletFound = true; walletFound = true;
} }
} else { } else {
if (await m0Legacy.wasEverUsed()) { if (await wasUsed(m0Legacy)) {
yield { wallet: m0Legacy }; yield { wallet: m0Legacy };
walletFound = true; walletFound = true;
} }
@ -275,7 +291,6 @@ const startImport = (
if (!walletFound) { if (!walletFound) {
yield { wallet: hd2 }; yield { wallet: hd2 };
} }
// return;
} }
yield { progress: 'wif' }; yield { progress: 'wif' };
@ -288,17 +303,17 @@ const startImport = (
yield { progress: 'wif p2wpkh' }; yield { progress: 'wif p2wpkh' };
const segwitBech32Wallet = new SegwitBech32Wallet(); const segwitBech32Wallet = new SegwitBech32Wallet();
segwitBech32Wallet.setSecret(text); segwitBech32Wallet.setSecret(text);
if (await segwitBech32Wallet.wasEverUsed()) { if (await wasUsed(segwitBech32Wallet)) {
// yep, its single-address bech32 wallet // yep, its single-address bech32 wallet
await segwitBech32Wallet.fetchBalance(); await fetch(segwitBech32Wallet, true);
walletFound = true; walletFound = true;
yield { wallet: segwitBech32Wallet }; yield { wallet: segwitBech32Wallet };
} }
yield { progress: 'wif p2wpkh-p2sh' }; yield { progress: 'wif p2wpkh-p2sh' };
if (await segwitWallet.wasEverUsed()) { if (await wasUsed(segwitWallet)) {
// yep, its single-address p2wpkh wallet // yep, its single-address p2wpkh wallet
await segwitWallet.fetchBalance(); await fetch(segwitWallet, true);
walletFound = true; walletFound = true;
yield { wallet: segwitWallet }; yield { wallet: segwitWallet };
} }
@ -307,9 +322,9 @@ const startImport = (
yield { progress: 'wif p2pkh' }; yield { progress: 'wif p2pkh' };
const legacyWallet = new LegacyWallet(); const legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text); legacyWallet.setSecret(text);
if (await legacyWallet.wasEverUsed()) { if (await wasUsed(legacyWallet)) {
// yep, its single-address legacy wallet // yep, its single-address legacy wallet
await legacyWallet.fetchBalance(); await fetch(legacyWallet, true);
walletFound = true; walletFound = true;
yield { wallet: legacyWallet }; yield { wallet: legacyWallet };
} }
@ -327,8 +342,7 @@ const startImport = (
const legacyWallet = new LegacyWallet(); const legacyWallet = new LegacyWallet();
legacyWallet.setSecret(text); legacyWallet.setSecret(text);
if (legacyWallet.getAddress()) { if (legacyWallet.getAddress()) {
await legacyWallet.fetchBalance(); await fetch(legacyWallet, true, true);
await legacyWallet.fetchTransactions();
yield { wallet: legacyWallet }; yield { wallet: legacyWallet };
} }
@ -337,7 +351,7 @@ const startImport = (
const watchOnly = new WatchOnlyWallet(); const watchOnly = new WatchOnlyWallet();
watchOnly.setSecret(text); watchOnly.setSecret(text);
if (watchOnly.valid()) { if (watchOnly.valid()) {
await watchOnly.fetchBalance(); await fetch(watchOnly, true);
yield { wallet: watchOnly }; yield { wallet: watchOnly };
} }
@ -384,7 +398,7 @@ const startImport = (
if (password) { if (password) {
s1.setPassphrase(password); s1.setPassphrase(password);
} }
if (await s1.wasEverUsed()) { if (await wasUsed(s1)) {
yield { wallet: s1 }; yield { wallet: s1 };
} }
@ -394,7 +408,7 @@ const startImport = (
s2.setPassphrase(password); s2.setPassphrase(password);
} }
s2.setSecret(text); s2.setSecret(text);
if (await s2.wasEverUsed()) { if (await wasUsed(s2)) {
yield { wallet: s2 }; yield { wallet: s2 };
} }
@ -433,6 +447,7 @@ const startImport = (
if (next.value?.progress) reportProgress(next.value.progress); if (next.value?.progress) reportProgress(next.value.progress);
if (next.value?.wallet) reportWallet(next.value.wallet); if (next.value?.wallet) reportWallet(next.value.wallet);
if (next.done) break; // break if generator has been finished if (next.done) break; // break if generator has been finished
await new Promise(resolve => setTimeout(resolve, 1)); // try not to block the thread
} }
reportFinish(); reportFinish();
})().catch(e => { })().catch(e => {

View File

@ -448,6 +448,7 @@
"import_discovery_subtitle": "Choose a discovered wallet", "import_discovery_subtitle": "Choose a discovered wallet",
"import_discovery_derivation": "Use custom derivation path", "import_discovery_derivation": "Use custom derivation path",
"import_discovery_no_wallets": "No wallets were found.", "import_discovery_no_wallets": "No wallets were found.",
"import_discovery_offline": "BlueWallet is currently in offline mode. In this mode, it can't verify the existence of the wallet, so you'll need to select the correct one manually",
"import_derivation_found": "Found", "import_derivation_found": "Found",
"import_derivation_found_not": "Not found", "import_derivation_found_not": "Not found",
"import_derivation_loading": "Loading...", "import_derivation_loading": "Loading...",

View File

@ -20,7 +20,11 @@ import {
export type AddWalletStackParamList = { export type AddWalletStackParamList = {
AddWallet: undefined; AddWallet: undefined;
ImportWallet: undefined; ImportWallet?: {
label?: string;
triggerImport?: boolean;
scannedData?: string;
};
ImportWalletDiscovery: { ImportWalletDiscovery: {
importText: string; importText: string;
askPassphrase: boolean; askPassphrase: boolean;

View File

@ -7,7 +7,7 @@ const WalletsAdd = lazy(() => import('../screen/wallets/Add'));
const ImportCustomDerivationPath = lazy(() => import('../screen/wallets/ImportCustomDerivationPath')); const ImportCustomDerivationPath = lazy(() => import('../screen/wallets/ImportCustomDerivationPath'));
const ImportWalletDiscovery = lazy(() => import('../screen/wallets/ImportWalletDiscovery')); const ImportWalletDiscovery = lazy(() => import('../screen/wallets/ImportWalletDiscovery'));
const ImportSpeed = lazy(() => import('../screen/wallets/ImportSpeed')); const ImportSpeed = lazy(() => import('../screen/wallets/ImportSpeed'));
const ImportWallet = lazy(() => import('../screen/wallets/import')); const ImportWallet = lazy(() => import('../screen/wallets/ImportWallet'));
const PleaseBackup = lazy(() => import('../screen/wallets/PleaseBackup')); const PleaseBackup = lazy(() => import('../screen/wallets/PleaseBackup'));
const PleaseBackupLNDHub = lazy(() => import('../screen/wallets/pleaseBackupLNDHub')); const PleaseBackupLNDHub = lazy(() => import('../screen/wallets/pleaseBackupLNDHub'));
const ProvideEntropy = lazy(() => import('../screen/wallets/ProvideEntropy')); const ProvideEntropy = lazy(() => import('../screen/wallets/ProvideEntropy'));

View File

@ -14,6 +14,7 @@ import WalletToImport from '../../components/WalletToImport';
import { useStorage } from '../../hooks/context/useStorage'; import { useStorage } from '../../hooks/context/useStorage';
import loc from '../../loc'; import loc from '../../loc';
import { AddWalletStackParamList } from '../../navigation/AddWalletStack'; import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
import { useSettings } from '../../hooks/context/useSettings';
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>; type RouteProps = RouteProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>;
type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>; type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>;
@ -44,6 +45,7 @@ const ImportCustomDerivationPath: React.FC = () => {
const [used, setUsed] = useState<TUsedByPath>({}); const [used, setUsed] = useState<TUsedByPath>({});
const [selected, setSelected] = useState<string>(''); const [selected, setSelected] = useState<string>('');
const importing = useRef(false); const importing = useRef(false);
const { isElectrumDisabled } = useSettings();
const debouncedSavePath = useRef( const debouncedSavePath = useRef(
debounce(async newPath => { debounce(async newPath => {
@ -65,6 +67,14 @@ const ImportCustomDerivationPath: React.FC = () => {
} }
setWallets(ws => ({ ...ws, [newPath]: newWallets })); setWallets(ws => ({ ...ws, [newPath]: newWallets }));
if (isElectrumDisabled) {
// do not check if electrum is disabled
Object.values(newWallets).forEach(w => {
setUsed(u => ({ ...u, [newPath]: { ...u[newPath], [w.type]: STATUS.WALLET_UNKNOWN } }));
});
return;
}
// discover was they ever used // discover was they ever used
const promises = Object.values(newWallets).map(w => { const promises = Object.values(newWallets).map(w => {
return w.wasEverUsed().then(v => { return w.wasEverUsed().then(v => {
@ -81,6 +91,7 @@ const ImportCustomDerivationPath: React.FC = () => {
} }
}, 500), }, 500),
); );
useEffect(() => { useEffect(() => {
if (path in wallets) return; if (path in wallets) return;
debouncedSavePath.current(path); debouncedSavePath.current(path);

View File

@ -1,36 +1,41 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useRoute } from '@react-navigation/native'; import { RouteProp, useRoute } from '@react-navigation/native';
import { Keyboard, Platform, StyleSheet, TouchableWithoutFeedback, View, ScrollView } from 'react-native'; import Clipboard from '@react-native-clipboard/clipboard';
import { Keyboard, Platform, ScrollView, StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
import { disallowScreenshot } from 'react-native-screen-capture';
import { BlueButtonLink, BlueFormLabel, BlueFormMultiInput, BlueSpacing20 } from '../../BlueComponents'; import { BlueButtonLink, BlueFormLabel, BlueFormMultiInput, BlueSpacing20 } from '../../BlueComponents';
import Button from '../../components/Button'; import Button from '../../components/Button';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import { disallowScreenshot } from 'react-native-screen-capture';
import loc from '../../loc';
import { import {
DoneAndDismissKeyboardInputAccessory, DoneAndDismissKeyboardInputAccessory,
DoneAndDismissKeyboardInputAccessoryViewID, DoneAndDismissKeyboardInputAccessoryViewID,
} from '../../components/DoneAndDismissKeyboardInputAccessory'; } from '../../components/DoneAndDismissKeyboardInputAccessory';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { useKeyboard } from '../../hooks/useKeyboard';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import Clipboard from '@react-native-clipboard/clipboard';
import HeaderMenuButton from '../../components/HeaderMenuButton'; import HeaderMenuButton from '../../components/HeaderMenuButton';
import { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
import { useSettings } from '../../hooks/context/useSettings'; import { useSettings } from '../../hooks/context/useSettings';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import { useKeyboard } from '../../hooks/useKeyboard';
import loc from '../../loc';
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
const WalletsImport = () => { type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWallet'>;
const navigation = useExtendedNavigation(); type NavigationProps = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWallet'>;
const ImportWallet = () => {
const navigation = useExtendedNavigation<NavigationProps>();
const { colors } = useTheme(); const { colors } = useTheme();
const route = useRoute(); const route = useRoute<RouteProps>();
const label = route?.params?.label ?? ''; const label = route?.params?.label ?? '';
const triggerImport = route?.params?.triggerImport ?? false; const triggerImport = route?.params?.triggerImport ?? false;
const scannedData = route?.params?.scannedData ?? ''; const scannedData = route?.params?.scannedData ?? '';
const [importText, setImportText] = useState(label); const [importText, setImportText] = useState<string>(label);
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false); const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState<boolean>(false);
const [, setSpeedBackdoor] = useState(0); const [, setSpeedBackdoor] = useState<number>(0);
const [searchAccountsMenuState, setSearchAccountsMenuState] = useState(false); const [searchAccountsMenuState, setSearchAccountsMenuState] = useState<boolean>(false);
const [askPassphraseMenuState, setAskPassphraseMenuState] = useState(false); const [askPassphraseMenuState, setAskPassphraseMenuState] = useState<boolean>(false);
const [clearClipboardMenuState, setClearClipboardMenuState] = useState(true); const [clearClipboardMenuState, setClearClipboardMenuState] = useState<boolean>(true);
const { isPrivacyBlurEnabled } = useSettings(); const { isPrivacyBlurEnabled } = useSettings();
// Styles // Styles
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -46,11 +51,11 @@ const WalletsImport = () => {
}, },
}); });
const onBlur = () => { const onBlur = useCallback(() => {
const valueWithSingleWhitespace = importText.replace(/^\s+|\s+$|\s+(?=\s)/g, ''); const valueWithSingleWhitespace = importText.replace(/^\s+|\s+$|\s+(?=\s)/g, '');
setImportText(valueWithSingleWhitespace); setImportText(valueWithSingleWhitespace);
return valueWithSingleWhitespace; return valueWithSingleWhitespace;
}; }, [importText]);
useKeyboard({ useKeyboard({
onKeyboardDidShow: () => { onKeyboardDidShow: () => {
@ -61,34 +66,8 @@ const WalletsImport = () => {
}, },
}); });
useEffect(() => { const importMnemonic = useCallback(
disallowScreenshot(isPrivacyBlurEnabled); async (text: string) => {
return () => {
disallowScreenshot(false);
};
}, [isPrivacyBlurEnabled]);
useEffect(() => {
if (triggerImport) importButtonPressed();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [triggerImport]);
useEffect(() => {
if (scannedData) {
onBarScanned(scannedData);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scannedData]);
const importButtonPressed = () => {
const textToImport = onBlur();
if (textToImport.trim().length === 0) {
return;
}
importMnemonic(textToImport);
};
const importMnemonic = async text => {
if (clearClipboardMenuState) { if (clearClipboardMenuState) {
try { try {
if (await Clipboard.hasString()) { if (await Clipboard.hasString()) {
@ -103,20 +82,34 @@ const WalletsImport = () => {
askPassphrase: askPassphraseMenuState, askPassphrase: askPassphraseMenuState,
searchAccounts: searchAccountsMenuState, searchAccounts: searchAccountsMenuState,
}); });
}; },
[askPassphraseMenuState, clearClipboardMenuState, navigation, searchAccountsMenuState],
);
const onBarScanned = value => { const handleImport = useCallback(() => {
if (value && value.data) value = value.data + ''; // no objects here, only strings const textToImport = onBlur();
setImportText(value); if (textToImport.trim().length === 0) {
setTimeout(() => importMnemonic(value), 500); return;
}; }
importMnemonic(textToImport);
}, [importMnemonic, onBlur]);
const importScan = async () => { const onBarScanned = useCallback(
const data = await scanQrHelper(navigation, true); (value: string | { data: any }) => {
// no objects here, only strings
const newValue: string = typeof value !== 'string' ? value.data + '' : value;
setImportText(newValue);
setTimeout(() => importMnemonic(newValue), 500);
},
[importMnemonic],
);
const importScan = useCallback(async () => {
const data = await scanQrHelper(route.name, true);
if (data) { if (data) {
onBarScanned(data); onBarScanned(data);
} }
}; }, [route.name, onBarScanned]);
const speedBackdoorTap = () => { const speedBackdoorTap = () => {
setSpeedBackdoor(v => { setSpeedBackdoor(v => {
@ -128,7 +121,7 @@ const WalletsImport = () => {
}; };
const toolTipOnPressMenuItem = useCallback( const toolTipOnPressMenuItem = useCallback(
menuItem => { (menuItem: string) => {
Keyboard.dismiss(); Keyboard.dismiss();
if (menuItem === CommonToolTipActions.Passphrase.id) { if (menuItem === CommonToolTipActions.Passphrase.id) {
setAskPassphraseMenuState(!askPassphraseMenuState); setAskPassphraseMenuState(!askPassphraseMenuState);
@ -143,13 +136,11 @@ const WalletsImport = () => {
// ToolTipMenu actions for advanced options // ToolTipMenu actions for advanced options
const toolTipActions = useMemo(() => { const toolTipActions = useMemo(() => {
const askPassphraseAction = CommonToolTipActions.Passphrase; return [
askPassphraseAction.menuState = askPassphraseMenuState; { ...CommonToolTipActions.Passphrase, menuState: askPassphraseMenuState },
const searchAccountsAction = CommonToolTipActions.SearchAccount; { ...CommonToolTipActions.SearchAccount, menuState: searchAccountsMenuState },
searchAccountsAction.menuState = searchAccountsMenuState; { ...CommonToolTipActions.ClearClipboard, menuState: clearClipboardMenuState },
const clearClipboardAction = CommonToolTipActions.ClearClipboard; ];
clearClipboardAction.menuState = clearClipboardMenuState;
return [askPassphraseAction, searchAccountsAction, clearClipboardAction];
}, [askPassphraseMenuState, clearClipboardMenuState, searchAccountsMenuState]); }, [askPassphraseMenuState, clearClipboardMenuState, searchAccountsMenuState]);
const HeaderRight = useMemo( const HeaderRight = useMemo(
@ -157,6 +148,23 @@ const WalletsImport = () => {
[toolTipOnPressMenuItem, toolTipActions], [toolTipOnPressMenuItem, toolTipActions],
); );
useEffect(() => {
disallowScreenshot(isPrivacyBlurEnabled);
return () => {
disallowScreenshot(false);
};
}, [isPrivacyBlurEnabled]);
useEffect(() => {
if (triggerImport) handleImport();
}, [triggerImport, handleImport]);
useEffect(() => {
if (scannedData) {
onBarScanned(scannedData);
}
}, [scannedData, onBarScanned]);
// Adding the ToolTipMenu to the header // Adding the ToolTipMenu to the header
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
@ -169,12 +177,7 @@ const WalletsImport = () => {
<BlueSpacing20 /> <BlueSpacing20 />
<View style={styles.center}> <View style={styles.center}>
<> <>
<Button <Button disabled={importText.trim().length === 0} title={loc.wallets.import_do_import} testID="DoImport" onPress={handleImport} />
disabled={importText.trim().length === 0}
title={loc.wallets.import_do_import}
testID="DoImport"
onPress={importButtonPressed}
/>
<BlueSpacing20 /> <BlueSpacing20 />
<BlueButtonLink title={loc.wallets.import_scan_qr} onPress={importScan} testID="ScanImport" /> <BlueButtonLink title={loc.wallets.import_scan_qr} onPress={importScan} testID="ScanImport" />
</> </>
@ -234,4 +237,4 @@ const WalletsImport = () => {
); );
}; };
export default WalletsImport; export default ImportWallet;

View File

@ -19,6 +19,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { THDWalletForWatchOnly, TWallet } from '../../class/wallets/types'; import { THDWalletForWatchOnly, TWallet } from '../../class/wallets/types';
import { navigate } from '../../NavigationService'; import { navigate } from '../../NavigationService';
import { keepAwake, disallowScreenshot } from 'react-native-screen-capture'; import { keepAwake, disallowScreenshot } from 'react-native-screen-capture';
import { useSettings } from '../../hooks/context/useSettings';
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWalletDiscovery'>; type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWalletDiscovery'>; type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
@ -34,6 +35,7 @@ const ImportWalletDiscovery: React.FC = () => {
const { colors } = useTheme(); const { colors } = useTheme();
const route = useRoute<RouteProps>(); const route = useRoute<RouteProps>();
const { importText, askPassphrase, searchAccounts } = route.params; const { importText, askPassphrase, searchAccounts } = route.params;
const { isElectrumDisabled } = useSettings();
const task = useRef<TImport | null>(null); const task = useRef<TImport | null>(null);
const { addAndSaveWallet } = useStorage(); const { addAndSaveWallet } = useStorage();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
@ -67,6 +69,11 @@ const ImportWalletDiscovery: React.FC = () => {
[addAndSaveWallet], [addAndSaveWallet],
); );
const handleSave = () => {
if (wallets.length === 0) return;
saveWallet(wallets[selected].wallet);
};
useEffect(() => { useEffect(() => {
const onProgress = (data: string) => setProgress(data); const onProgress = (data: string) => setProgress(data);
@ -105,7 +112,7 @@ const ImportWalletDiscovery: React.FC = () => {
}; };
keepAwake(true); keepAwake(true);
task.current = startImport(importText, askPassphrase, searchAccounts, onProgress, onWallet, onPassword); task.current = startImport(importText, askPassphrase, searchAccounts, isElectrumDisabled, onProgress, onWallet, onPassword);
task.current.promise task.current.promise
.then(({ cancelled, wallets: w }) => { .then(({ cancelled, wallets: w }) => {
@ -117,6 +124,7 @@ const ImportWalletDiscovery: React.FC = () => {
}) })
.catch(e => { .catch(e => {
console.warn('import error', e); console.warn('import error', e);
console.warn('err.stack', e.stack);
presentAlert({ title: 'Import error', message: e.message }); presentAlert({ title: 'Import error', message: e.message });
}) })
.finally(() => { .finally(() => {
@ -129,8 +137,9 @@ const ImportWalletDiscovery: React.FC = () => {
keepAwake(false); keepAwake(false);
task.current?.stop(); task.current?.stop();
}; };
// ignoring "navigation" here, because it is constantly mutating
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [askPassphrase, importText, isElectrumDisabled, saveWallet, searchAccounts]);
const handleCustomDerivation = () => { const handleCustomDerivation = () => {
task.current?.stop(); task.current?.stop();
@ -157,16 +166,21 @@ const ImportWalletDiscovery: React.FC = () => {
const ListHeaderComponent = useMemo( const ListHeaderComponent = useMemo(
() => ( () => (
<> <>
{wallets && wallets.length > 0 ? ( {wallets.length > 0 ? (
<> <>
{isElectrumDisabled && (
<>
<BlueFormLabel>{loc.wallets.import_discovery_offline}</BlueFormLabel>
<BlueSpacing20 /> <BlueSpacing20 />
</>
)}
<BlueFormLabel>{loc.wallets.import_discovery_subtitle}</BlueFormLabel> <BlueFormLabel>{loc.wallets.import_discovery_subtitle}</BlueFormLabel>
<BlueSpacing10 /> <BlueSpacing10 />
</> </>
) : null} ) : null}
</> </>
), ),
[wallets], [wallets, isElectrumDisabled],
); );
const ListEmptyComponent = useMemo( const ListEmptyComponent = useMemo(
@ -213,14 +227,7 @@ const ImportWalletDiscovery: React.FC = () => {
)} )}
<BlueSpacing10 /> <BlueSpacing10 />
<View style={styles.buttonContainer}> <View style={styles.buttonContainer}>
<Button <Button disabled={wallets?.length === 0} title={loc.wallets.import_do_import} onPress={handleSave} />
disabled={wallets?.length === 0}
title={loc.wallets.import_do_import}
onPress={() => {
if (wallets.length === 0) return;
saveWallet(wallets[selected].wallet);
}}
/>
</View> </View>
</View> </View>
</SafeArea> </SafeArea>

View File

@ -1,4 +1,5 @@
import assert from 'assert'; import assert from 'assert';
import fs from 'fs';
import * as BlueElectrum from '../../blue_modules/BlueElectrum'; import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import { import {
@ -17,7 +18,7 @@ import {
WatchOnlyWallet, WatchOnlyWallet,
} from '../../class'; } from '../../class';
import startImport from '../../class/wallet-import'; import startImport from '../../class/wallet-import';
const fs = require('fs'); import { TWallet } from '../../class/wallets/types';
jest.setTimeout(90 * 1000); jest.setTimeout(90 * 1000);
@ -32,31 +33,37 @@ beforeAll(async () => {
await BlueElectrum.connectMain(); await BlueElectrum.connectMain();
}); });
const createStore = password => { type THistoryItem = { action: 'progress'; data: string } | { action: 'wallet'; data: TWallet } | { action: 'password'; data: string };
const state = { wallets: [] }; type TState = { wallets: TWallet[]; progress?: string; password?: string };
const history = []; type TOnProgress = (name: string) => void;
type TOnWallet = (wallet: TWallet) => void;
type TOnPassword = (title: string, text: string) => Promise<string>;
const onProgress = data => { const createStore = (password?: string) => {
const state: TState = { wallets: [] };
const history: THistoryItem[] = [];
const onProgress: TOnProgress = data => {
history.push({ action: 'progress', data }); history.push({ action: 'progress', data });
state.progress = data; state.progress = data;
}; };
const onWallet = data => { const onWallet: TOnWallet = data => {
history.push({ action: 'wallet', data }); history.push({ action: 'wallet', data });
state.wallets.push(data); state.wallets.push(data);
}; };
const onPassword = () => { const onPassword: TOnPassword = async () => {
history.push({ action: 'password', data: password }); history.push({ action: 'password', data: password! });
state.password = password; state.password = password;
return password; return password!;
}; };
return { return {
state, state,
history, history,
callbacks: [onProgress, onWallet, onPassword], callbacks: [onProgress, onWallet, onPassword],
}; } as const;
}; };
describe('import procedure', () => { describe('import procedure', () => {
@ -69,8 +76,9 @@ describe('import procedure', () => {
return undefined; return undefined;
}; };
const store = createStore(); const store = createStore();
// @ts-ignore: oopsie
store.callbacks[2] = onPassword; store.callbacks[2] = onPassword;
const { promise } = startImport('6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN', false, false, ...store.callbacks); const { promise } = startImport('6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN', false, false, false, ...store.callbacks);
const imprt = await promise; const imprt = await promise;
assert.strictEqual(store.state.wallets.length, 0); assert.strictEqual(store.state.wallets.length, 0);
assert.strictEqual(imprt.cancelled, true); assert.strictEqual(imprt.cancelled, true);
@ -78,7 +86,7 @@ describe('import procedure', () => {
it('can be stopped', async () => { it('can be stopped', async () => {
const store = createStore(); const store = createStore();
const { promise, stop } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, ...store.callbacks); const { promise, stop } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, false, ...store.callbacks);
stop(); stop();
await assert.doesNotReject(async () => await promise); await assert.doesNotReject(async () => await promise);
const imprt = await promise; const imprt = await promise;
@ -91,18 +99,33 @@ describe('import procedure', () => {
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
false, false,
true, true,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
assert.strictEqual(store.state.wallets.length > 3, true); assert.strictEqual(store.state.wallets.length > 3, true);
}); });
it('can import multiple wallets in offline mode', async () => {
const store = createStore();
const { promise } = startImport(
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
false,
true,
true,
...store.callbacks,
);
await promise;
assert.strictEqual(store.state.wallets.length > 100, true);
});
it('can import BIP84', async () => { it('can import BIP84', async () => {
const store = createStore(); const store = createStore();
const { promise } = startImport( const { promise } = startImport(
'always direct find escape liar turn differ shy tool gap elder galaxy lawn wild movie fog moon spread casual inner box diagram outdoor tell', 'always direct find escape liar turn differ shy tool gap elder galaxy lawn wild movie fog moon spread casual inner box diagram outdoor tell',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -116,6 +139,7 @@ describe('import procedure', () => {
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
true, true,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -125,7 +149,7 @@ describe('import procedure', () => {
it('can import Legacy', async () => { it('can import Legacy', async () => {
const store = createStore(); const store = createStore();
const { promise } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, ...store.callbacks); const { promise } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].type, LegacyWallet.type); assert.strictEqual(store.state.wallets[0].type, LegacyWallet.type);
assert.strictEqual(store.state.wallets[0].getAddress(), '1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78'); assert.strictEqual(store.state.wallets[0].getAddress(), '1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78');
@ -133,7 +157,7 @@ describe('import procedure', () => {
it('can import P2SH Segwit', async () => { it('can import P2SH Segwit', async () => {
const store = createStore(); const store = createStore();
const { promise } = startImport('L3NxFnYoBGjJ5PhxrxV6jorvjnc8cerYJx71vXU6ta8BXQxHVZya', false, false, ...store.callbacks); const { promise } = startImport('L3NxFnYoBGjJ5PhxrxV6jorvjnc8cerYJx71vXU6ta8BXQxHVZya', false, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].type, SegwitP2SHWallet.type); assert.strictEqual(store.state.wallets[0].type, SegwitP2SHWallet.type);
assert.strictEqual(store.state.wallets[0].getAddress(), '3KM9VfdsDf9uT7uwZagoKgVn8z35m9CtSM'); assert.strictEqual(store.state.wallets[0].getAddress(), '3KM9VfdsDf9uT7uwZagoKgVn8z35m9CtSM');
@ -143,7 +167,7 @@ describe('import procedure', () => {
it('can import Bech32 Segwit', async () => { it('can import Bech32 Segwit', async () => {
const store = createStore(); const store = createStore();
const { promise } = startImport('L1T6FfKpKHi8JE6eBKrsXkenw34d5FfFzJUZ6dLs2utxkSvsDfxZ', false, false, ...store.callbacks); const { promise } = startImport('L1T6FfKpKHi8JE6eBKrsXkenw34d5FfFzJUZ6dLs2utxkSvsDfxZ', false, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type); assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type);
assert.strictEqual(store.state.wallets[0].getAddress(), 'bc1q763rf54hzuncmf8dtlz558uqe4f247mq39rjvr'); assert.strictEqual(store.state.wallets[0].getAddress(), 'bc1q763rf54hzuncmf8dtlz558uqe4f247mq39rjvr');
@ -153,7 +177,7 @@ describe('import procedure', () => {
it('can import Legacy/P2SH/Bech32 from an empty wallet', async () => { it('can import Legacy/P2SH/Bech32 from an empty wallet', async () => {
const store = createStore(); const store = createStore();
const { promise } = startImport('L36mabzoQyMZoHHsBFVNB7PUBXgXTynwY6yR7kYZ82EkS7oejVp2', false, false, ...store.callbacks); const { promise } = startImport('L36mabzoQyMZoHHsBFVNB7PUBXgXTynwY6yR7kYZ82EkS7oejVp2', false, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type); assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type);
assert.strictEqual(store.state.wallets[0].getAddress(), 'bc1q8dkdgpaq9sd2xwptsjhe7krwp0k595w0hdtkfr'); assert.strictEqual(store.state.wallets[0].getAddress(), 'bc1q8dkdgpaq9sd2xwptsjhe7krwp0k595w0hdtkfr');
@ -169,6 +193,7 @@ describe('import procedure', () => {
'sting museum endless duty nice riot because swallow brother depth weapon merge woman wish hold finish venture gauge stomach bomb device bracket agent parent', 'sting museum endless duty nice riot because swallow brother depth weapon merge woman wish hold finish venture gauge stomach bomb device bracket agent parent',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -182,6 +207,7 @@ describe('import procedure', () => {
'abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abeille', 'abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abeille',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -195,6 +221,7 @@ describe('import procedure', () => {
'believe torch sport lizard absurd retreat scale layer song pen clump combine window staff dream filter latin bicycle vapor anchor put clean gain slush', 'believe torch sport lizard absurd retreat scale layer song pen clump combine window staff dream filter latin bicycle vapor anchor put clean gain slush',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -208,6 +235,7 @@ describe('import procedure', () => {
'eight derive blast guide smoke piece coral burden lottery flower tomato flame', 'eight derive blast guide smoke piece coral burden lottery flower tomato flame',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -221,6 +249,7 @@ describe('import procedure', () => {
'receive happy wash prosper update pet neck acid try profit proud hungry', 'receive happy wash prosper update pet neck acid try profit proud hungry',
true, true,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -234,6 +263,7 @@ describe('import procedure', () => {
'become salmon motor battle sweet merit romance ecology age squirrel oblige awesome', 'become salmon motor battle sweet merit romance ecology age squirrel oblige awesome',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -248,6 +278,7 @@ describe('import procedure', () => {
'noble mimic pipe merry knife screen enter dune crop bonus slice card', 'noble mimic pipe merry knife screen enter dune crop bonus slice card',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -262,6 +293,7 @@ describe('import procedure', () => {
'bitter grass shiver impose acquire brush forget axis eager alone wine silver', 'bitter grass shiver impose acquire brush forget axis eager alone wine silver',
true, true,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -275,6 +307,7 @@ describe('import procedure', () => {
'abstract rhythm weird food attract treat mosquito sight royal actor surround ride strike remove guilt catch filter summer mushroom protect poverty cruel chaos pattern', 'abstract rhythm weird food attract treat mosquito sight royal actor surround ride strike remove guilt catch filter summer mushroom protect poverty cruel chaos pattern',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -287,6 +320,7 @@ describe('import procedure', () => {
'able mix price funny host express lawsuit congress antique float pig exchange vapor drip wide cup style apple tumble verb fix blush tongue market', 'able mix price funny host express lawsuit congress antique float pig exchange vapor drip wide cup style apple tumble verb fix blush tongue market',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -297,14 +331,14 @@ describe('import procedure', () => {
const store = createStore(); const store = createStore();
const tempWallet = new HDSegwitBech32Wallet(); const tempWallet = new HDSegwitBech32Wallet();
await tempWallet.generate(); await tempWallet.generate();
const { promise } = startImport(tempWallet.getSecret(), false, false, ...store.callbacks); const { promise } = startImport(tempWallet.getSecret(), false, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].type, HDSegwitBech32Wallet.type); assert.strictEqual(store.state.wallets[0].type, HDSegwitBech32Wallet.type);
}); });
it('can import Legacy with uncompressed pubkey', async () => { it('can import Legacy with uncompressed pubkey', async () => {
const store = createStore(); const store = createStore();
const { promise } = startImport('5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS', false, false, ...store.callbacks); const { promise } = startImport('5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS', false, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].getSecret(), '5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS'); assert.strictEqual(store.state.wallets[0].getSecret(), '5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS');
assert.strictEqual(store.state.wallets[0].type, LegacyWallet.type); assert.strictEqual(store.state.wallets[0].type, LegacyWallet.type);
@ -313,7 +347,7 @@ describe('import procedure', () => {
it('can import BIP38 encrypted backup', async () => { it('can import BIP38 encrypted backup', async () => {
const store = createStore('qwerty'); const store = createStore('qwerty');
const { promise } = startImport('6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN', false, false, ...store.callbacks); const { promise } = startImport('6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN', false, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].getSecret(), 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc'); assert.strictEqual(store.state.wallets[0].getSecret(), 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc');
assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type); assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type);
@ -328,17 +362,17 @@ describe('import procedure', () => {
it('can import watch-only address', async () => { it('can import watch-only address', async () => {
const store1 = createStore(); const store1 = createStore();
const { promise: promise1 } = startImport('1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78', false, false, ...store1.callbacks); const { promise: promise1 } = startImport('1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78', false, false, false, ...store1.callbacks);
await promise1; await promise1;
assert.strictEqual(store1.state.wallets[0].type, WatchOnlyWallet.type); assert.strictEqual(store1.state.wallets[0].type, WatchOnlyWallet.type);
const store2 = createStore(); const store2 = createStore();
const { promise: promise2 } = startImport('3EoqYYp7hQSHn5nHqRtWzkgqmK3caQ2SUu', false, false, ...store2.callbacks); const { promise: promise2 } = startImport('3EoqYYp7hQSHn5nHqRtWzkgqmK3caQ2SUu', false, false, false, ...store2.callbacks);
await promise2; await promise2;
assert.strictEqual(store2.state.wallets[0].type, WatchOnlyWallet.type); assert.strictEqual(store2.state.wallets[0].type, WatchOnlyWallet.type);
const store3 = createStore(); const store3 = createStore();
const { promise: promise3 } = startImport('bc1q8j4lk4qlhun0n7h5ahfslfldc8zhlxgynfpdj2', false, false, ...store3.callbacks); const { promise: promise3 } = startImport('bc1q8j4lk4qlhun0n7h5ahfslfldc8zhlxgynfpdj2', false, false, false, ...store3.callbacks);
await promise3; await promise3;
assert.strictEqual(store3.state.wallets[0].type, WatchOnlyWallet.type); assert.strictEqual(store3.state.wallets[0].type, WatchOnlyWallet.type);
@ -347,6 +381,7 @@ describe('import procedure', () => {
'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP', 'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP',
false, false,
false, false,
false,
...store4.callbacks, ...store4.callbacks,
); );
await promise4; await promise4;
@ -364,6 +399,7 @@ describe('import procedure', () => {
'crystal lungs academic agency class payment actress avoid rebound ordinary exchange petition tendency mild mobile spine robin fancy shelter increase', 'crystal lungs academic agency class payment actress avoid rebound ordinary exchange petition tendency mild mobile spine robin fancy shelter increase',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -381,6 +417,7 @@ describe('import procedure', () => {
'crystal lungs academic agency class payment actress avoid rebound ordinary exchange petition tendency mild mobile spine robin fancy shelter increase', 'crystal lungs academic agency class payment actress avoid rebound ordinary exchange petition tendency mild mobile spine robin fancy shelter increase',
true, true,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -394,6 +431,7 @@ describe('import procedure', () => {
'{"ExtPubKey":"zpub6riZchHnrWzhhZ3Z4dhCJmesGyafMmZBRC9txhnidR313XJbcv4KiDubderKHhL7rMsqacYd82FQ38e4whgs8Dg7CpsxX3dSGWayXsEerF4","MasterFingerprint":"7D2F0272","AccountKeyPath":"84\'\\/0\'\\/0\'","CoboVaultFirmwareVersion":"2.6.1(BTC-Only)"}', '{"ExtPubKey":"zpub6riZchHnrWzhhZ3Z4dhCJmesGyafMmZBRC9txhnidR313XJbcv4KiDubderKHhL7rMsqacYd82FQ38e4whgs8Dg7CpsxX3dSGWayXsEerF4","MasterFingerprint":"7D2F0272","AccountKeyPath":"84\'\\/0\'\\/0\'","CoboVaultFirmwareVersion":"2.6.1(BTC-Only)"}',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -408,6 +446,7 @@ describe('import procedure', () => {
`[{"ExtPubKey":"zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/84'/0'/0'"},{"ExtPubKey":"ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/49'/0'/0'"},{"ExtPubKey":"xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/44'/0'/0'"}]`, `[{"ExtPubKey":"zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/84'/0'/0'"},{"ExtPubKey":"ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/49'/0'/0'"},{"ExtPubKey":"xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj","MasterFingerprint":"73C5DA0A","AccountKeyPath":"m/44'/0'/0'"}]`,
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -442,6 +481,7 @@ describe('import procedure', () => {
'{"ExtPubKey":"zpub6qT7amLcp2exr4mU4AhXZMjD9CFkopECVhUxc9LHW8pNsJG2B9ogs5sFbGZpxEeT5TBjLmc7EFYgZA9EeWEM1xkJMFLefzZc8eigRFhKB8Q","MasterFingerprint":"01EBDA7D","AccountKeyPath":"m/84\'/0\'/0\'"}', '{"ExtPubKey":"zpub6qT7amLcp2exr4mU4AhXZMjD9CFkopECVhUxc9LHW8pNsJG2B9ogs5sFbGZpxEeT5TBjLmc7EFYgZA9EeWEM1xkJMFLefzZc8eigRFhKB8Q","MasterFingerprint":"01EBDA7D","AccountKeyPath":"m/84\'/0\'/0\'"}',
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -456,6 +496,7 @@ describe('import procedure', () => {
'trip ener cloc puls hams ghos inha crow inju vibr seve chro', 'trip ener cloc puls hams ghos inha crow inju vibr seve chro',
false, false,
false, false,
false,
...store1.callbacks, ...store1.callbacks,
); );
await promise1; await promise1;
@ -470,6 +511,7 @@ describe('import procedure', () => {
'docu gosp razo chao nort ches nomi fati swam firs deca boy icon virt gap prep seri anch', 'docu gosp razo chao nort ches nomi fati swam firs deca boy icon virt gap prep seri anch',
false, false,
false, false,
false,
...store2.callbacks, ...store2.callbacks,
); );
await promise2; await promise2;
@ -484,6 +526,7 @@ describe('import procedure', () => {
'rece own flig sent tide hood sile bunk deri mana wink belt loud apol mons pill raw gate hurd matc nigh wish todd achi', 'rece own flig sent tide hood sile bunk deri mana wink belt loud apol mons pill raw gate hurd matc nigh wish todd achi',
false, false,
false, false,
false,
...store3.callbacks, ...store3.callbacks,
); );
await promise3; await promise3;
@ -500,7 +543,7 @@ describe('import procedure', () => {
} }
const store = createStore('1'); const store = createStore('1');
const { promise } = startImport(process.env.BIP47_HD_MNEMONIC.split(':')[0], true, false, ...store.callbacks); const { promise } = startImport(process.env.BIP47_HD_MNEMONIC.split(':')[0], true, false, false, ...store.callbacks);
await promise; await promise;
assert.strictEqual(store.state.wallets[0].type, HDLegacyP2PKHWallet.type); assert.strictEqual(store.state.wallets[0].type, HDLegacyP2PKHWallet.type);
assert.strictEqual(store.state.wallets[1].type, HDSegwitBech32Wallet.type); assert.strictEqual(store.state.wallets[1].type, HDSegwitBech32Wallet.type);
@ -513,6 +556,7 @@ describe('import procedure', () => {
fs.readFileSync('tests/unit/fixtures/coldcardmk4/descriptor.txt').toString('utf8'), fs.readFileSync('tests/unit/fixtures/coldcardmk4/descriptor.txt').toString('utf8'),
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -530,6 +574,7 @@ describe('import procedure', () => {
fs.readFileSync('tests/unit/fixtures/coldcardmk4/new-wasabi.json').toString('utf8'), fs.readFileSync('tests/unit/fixtures/coldcardmk4/new-wasabi.json').toString('utf8'),
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;
@ -547,6 +592,7 @@ describe('import procedure', () => {
fs.readFileSync('tests/unit/fixtures/coldcardmk4/sparrow-export.json').toString('utf8'), fs.readFileSync('tests/unit/fixtures/coldcardmk4/sparrow-export.json').toString('utf8'),
false, false,
false, false,
false,
...store.callbacks, ...store.callbacks,
); );
await promise; await promise;