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 => {
mainClient.close();
mainClient?.close();
};
export const setBatchingDisabled = () => {

View File

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

View File

@ -448,6 +448,7 @@
"import_discovery_subtitle": "Choose a discovered wallet",
"import_discovery_derivation": "Use custom derivation path",
"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_not": "Not found",
"import_derivation_loading": "Loading...",

View File

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

View File

@ -7,7 +7,7 @@ const WalletsAdd = lazy(() => import('../screen/wallets/Add'));
const ImportCustomDerivationPath = lazy(() => import('../screen/wallets/ImportCustomDerivationPath'));
const ImportWalletDiscovery = lazy(() => import('../screen/wallets/ImportWalletDiscovery'));
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 PleaseBackupLNDHub = lazy(() => import('../screen/wallets/pleaseBackupLNDHub'));
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 loc from '../../loc';
import { AddWalletStackParamList } from '../../navigation/AddWalletStack';
import { useSettings } from '../../hooks/context/useSettings';
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>;
type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>;
@ -44,6 +45,7 @@ const ImportCustomDerivationPath: React.FC = () => {
const [used, setUsed] = useState<TUsedByPath>({});
const [selected, setSelected] = useState<string>('');
const importing = useRef(false);
const { isElectrumDisabled } = useSettings();
const debouncedSavePath = useRef(
debounce(async newPath => {
@ -65,6 +67,14 @@ const ImportCustomDerivationPath: React.FC = () => {
}
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
const promises = Object.values(newWallets).map(w => {
return w.wasEverUsed().then(v => {
@ -81,6 +91,7 @@ const ImportCustomDerivationPath: React.FC = () => {
}
}, 500),
);
useEffect(() => {
if (path in wallets) return;
debouncedSavePath.current(path);

View File

@ -1,36 +1,41 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { useRoute } from '@react-navigation/native';
import { Keyboard, Platform, StyleSheet, TouchableWithoutFeedback, View, ScrollView } from 'react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { RouteProp, useRoute } from '@react-navigation/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 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 {
DoneAndDismissKeyboardInputAccessory,
DoneAndDismissKeyboardInputAccessoryViewID,
} 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 { useTheme } from '../../components/themes';
import { scanQrHelper } from '../../helpers/scan-qr';
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 = () => {
const navigation = useExtendedNavigation();
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWallet'>;
type NavigationProps = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWallet'>;
const ImportWallet = () => {
const navigation = useExtendedNavigation<NavigationProps>();
const { colors } = useTheme();
const route = useRoute();
const route = useRoute<RouteProps>();
const label = route?.params?.label ?? '';
const triggerImport = route?.params?.triggerImport ?? false;
const scannedData = route?.params?.scannedData ?? '';
const [importText, setImportText] = useState(label);
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState(false);
const [, setSpeedBackdoor] = useState(0);
const [searchAccountsMenuState, setSearchAccountsMenuState] = useState(false);
const [askPassphraseMenuState, setAskPassphraseMenuState] = useState(false);
const [clearClipboardMenuState, setClearClipboardMenuState] = useState(true);
const [importText, setImportText] = useState<string>(label);
const [isToolbarVisibleForAndroid, setIsToolbarVisibleForAndroid] = useState<boolean>(false);
const [, setSpeedBackdoor] = useState<number>(0);
const [searchAccountsMenuState, setSearchAccountsMenuState] = useState<boolean>(false);
const [askPassphraseMenuState, setAskPassphraseMenuState] = useState<boolean>(false);
const [clearClipboardMenuState, setClearClipboardMenuState] = useState<boolean>(true);
const { isPrivacyBlurEnabled } = useSettings();
// Styles
const styles = StyleSheet.create({
@ -46,11 +51,11 @@ const WalletsImport = () => {
},
});
const onBlur = () => {
const onBlur = useCallback(() => {
const valueWithSingleWhitespace = importText.replace(/^\s+|\s+$|\s+(?=\s)/g, '');
setImportText(valueWithSingleWhitespace);
return valueWithSingleWhitespace;
};
}, [importText]);
useKeyboard({
onKeyboardDidShow: () => {
@ -61,34 +66,8 @@ const WalletsImport = () => {
},
});
useEffect(() => {
disallowScreenshot(isPrivacyBlurEnabled);
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 => {
const importMnemonic = useCallback(
async (text: string) => {
if (clearClipboardMenuState) {
try {
if (await Clipboard.hasString()) {
@ -103,20 +82,34 @@ const WalletsImport = () => {
askPassphrase: askPassphraseMenuState,
searchAccounts: searchAccountsMenuState,
});
};
},
[askPassphraseMenuState, clearClipboardMenuState, navigation, searchAccountsMenuState],
);
const onBarScanned = value => {
if (value && value.data) value = value.data + ''; // no objects here, only strings
setImportText(value);
setTimeout(() => importMnemonic(value), 500);
};
const handleImport = useCallback(() => {
const textToImport = onBlur();
if (textToImport.trim().length === 0) {
return;
}
importMnemonic(textToImport);
}, [importMnemonic, onBlur]);
const importScan = async () => {
const data = await scanQrHelper(navigation, true);
const onBarScanned = useCallback(
(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) {
onBarScanned(data);
}
};
}, [route.name, onBarScanned]);
const speedBackdoorTap = () => {
setSpeedBackdoor(v => {
@ -128,7 +121,7 @@ const WalletsImport = () => {
};
const toolTipOnPressMenuItem = useCallback(
menuItem => {
(menuItem: string) => {
Keyboard.dismiss();
if (menuItem === CommonToolTipActions.Passphrase.id) {
setAskPassphraseMenuState(!askPassphraseMenuState);
@ -143,13 +136,11 @@ const WalletsImport = () => {
// ToolTipMenu actions for advanced options
const toolTipActions = useMemo(() => {
const askPassphraseAction = CommonToolTipActions.Passphrase;
askPassphraseAction.menuState = askPassphraseMenuState;
const searchAccountsAction = CommonToolTipActions.SearchAccount;
searchAccountsAction.menuState = searchAccountsMenuState;
const clearClipboardAction = CommonToolTipActions.ClearClipboard;
clearClipboardAction.menuState = clearClipboardMenuState;
return [askPassphraseAction, searchAccountsAction, clearClipboardAction];
return [
{ ...CommonToolTipActions.Passphrase, menuState: askPassphraseMenuState },
{ ...CommonToolTipActions.SearchAccount, menuState: searchAccountsMenuState },
{ ...CommonToolTipActions.ClearClipboard, menuState: clearClipboardMenuState },
];
}, [askPassphraseMenuState, clearClipboardMenuState, searchAccountsMenuState]);
const HeaderRight = useMemo(
@ -157,6 +148,23 @@ const WalletsImport = () => {
[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
useEffect(() => {
navigation.setOptions({
@ -169,12 +177,7 @@ const WalletsImport = () => {
<BlueSpacing20 />
<View style={styles.center}>
<>
<Button
disabled={importText.trim().length === 0}
title={loc.wallets.import_do_import}
testID="DoImport"
onPress={importButtonPressed}
/>
<Button disabled={importText.trim().length === 0} title={loc.wallets.import_do_import} testID="DoImport" onPress={handleImport} />
<BlueSpacing20 />
<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 { navigate } from '../../NavigationService';
import { keepAwake, disallowScreenshot } from 'react-native-screen-capture';
import { useSettings } from '../../hooks/context/useSettings';
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportWalletDiscovery'>;
@ -34,6 +35,7 @@ const ImportWalletDiscovery: React.FC = () => {
const { colors } = useTheme();
const route = useRoute<RouteProps>();
const { importText, askPassphrase, searchAccounts } = route.params;
const { isElectrumDisabled } = useSettings();
const task = useRef<TImport | null>(null);
const { addAndSaveWallet } = useStorage();
const [loading, setLoading] = useState<boolean>(true);
@ -67,6 +69,11 @@ const ImportWalletDiscovery: React.FC = () => {
[addAndSaveWallet],
);
const handleSave = () => {
if (wallets.length === 0) return;
saveWallet(wallets[selected].wallet);
};
useEffect(() => {
const onProgress = (data: string) => setProgress(data);
@ -105,7 +112,7 @@ const ImportWalletDiscovery: React.FC = () => {
};
keepAwake(true);
task.current = startImport(importText, askPassphrase, searchAccounts, onProgress, onWallet, onPassword);
task.current = startImport(importText, askPassphrase, searchAccounts, isElectrumDisabled, onProgress, onWallet, onPassword);
task.current.promise
.then(({ cancelled, wallets: w }) => {
@ -117,6 +124,7 @@ const ImportWalletDiscovery: React.FC = () => {
})
.catch(e => {
console.warn('import error', e);
console.warn('err.stack', e.stack);
presentAlert({ title: 'Import error', message: e.message });
})
.finally(() => {
@ -129,8 +137,9 @@ const ImportWalletDiscovery: React.FC = () => {
keepAwake(false);
task.current?.stop();
};
// ignoring "navigation" here, because it is constantly mutating
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [askPassphrase, importText, isElectrumDisabled, saveWallet, searchAccounts]);
const handleCustomDerivation = () => {
task.current?.stop();
@ -157,16 +166,21 @@ const ImportWalletDiscovery: React.FC = () => {
const ListHeaderComponent = useMemo(
() => (
<>
{wallets && wallets.length > 0 ? (
{wallets.length > 0 ? (
<>
{isElectrumDisabled && (
<>
<BlueFormLabel>{loc.wallets.import_discovery_offline}</BlueFormLabel>
<BlueSpacing20 />
</>
)}
<BlueFormLabel>{loc.wallets.import_discovery_subtitle}</BlueFormLabel>
<BlueSpacing10 />
</>
) : null}
</>
),
[wallets],
[wallets, isElectrumDisabled],
);
const ListEmptyComponent = useMemo(
@ -213,14 +227,7 @@ const ImportWalletDiscovery: React.FC = () => {
)}
<BlueSpacing10 />
<View style={styles.buttonContainer}>
<Button
disabled={wallets?.length === 0}
title={loc.wallets.import_do_import}
onPress={() => {
if (wallets.length === 0) return;
saveWallet(wallets[selected].wallet);
}}
/>
<Button disabled={wallets?.length === 0} title={loc.wallets.import_do_import} onPress={handleSave} />
</View>
</View>
</SafeArea>

View File

@ -1,4 +1,5 @@
import assert from 'assert';
import fs from 'fs';
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
import {
@ -17,7 +18,7 @@ import {
WatchOnlyWallet,
} from '../../class';
import startImport from '../../class/wallet-import';
const fs = require('fs');
import { TWallet } from '../../class/wallets/types';
jest.setTimeout(90 * 1000);
@ -32,31 +33,37 @@ beforeAll(async () => {
await BlueElectrum.connectMain();
});
const createStore = password => {
const state = { wallets: [] };
const history = [];
type THistoryItem = { action: 'progress'; data: string } | { action: 'wallet'; data: TWallet } | { action: 'password'; data: string };
type TState = { wallets: TWallet[]; progress?: string; password?: string };
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 });
state.progress = data;
};
const onWallet = data => {
const onWallet: TOnWallet = data => {
history.push({ action: 'wallet', data });
state.wallets.push(data);
};
const onPassword = () => {
history.push({ action: 'password', data: password });
const onPassword: TOnPassword = async () => {
history.push({ action: 'password', data: password! });
state.password = password;
return password;
return password!;
};
return {
state,
history,
callbacks: [onProgress, onWallet, onPassword],
};
} as const;
};
describe('import procedure', () => {
@ -69,8 +76,9 @@ describe('import procedure', () => {
return undefined;
};
const store = createStore();
// @ts-ignore: oopsie
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;
assert.strictEqual(store.state.wallets.length, 0);
assert.strictEqual(imprt.cancelled, true);
@ -78,7 +86,7 @@ describe('import procedure', () => {
it('can be stopped', async () => {
const store = createStore();
const { promise, stop } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, ...store.callbacks);
const { promise, stop } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, false, ...store.callbacks);
stop();
await assert.doesNotReject(async () => 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',
false,
true,
false,
...store.callbacks,
);
await promise;
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 () => {
const store = createStore();
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',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -116,6 +139,7 @@ describe('import procedure', () => {
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
true,
false,
false,
...store.callbacks,
);
await promise;
@ -125,7 +149,7 @@ describe('import procedure', () => {
it('can import Legacy', async () => {
const store = createStore();
const { promise } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, ...store.callbacks);
const { promise } = startImport('KztVRmc2EJJBHi599mCdXrxMTsNsGy3NUjc3Fb3FFDSMYyMDRjnv', false, false, false, ...store.callbacks);
await promise;
assert.strictEqual(store.state.wallets[0].type, LegacyWallet.type);
assert.strictEqual(store.state.wallets[0].getAddress(), '1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78');
@ -133,7 +157,7 @@ describe('import procedure', () => {
it('can import P2SH Segwit', async () => {
const store = createStore();
const { promise } = startImport('L3NxFnYoBGjJ5PhxrxV6jorvjnc8cerYJx71vXU6ta8BXQxHVZya', false, false, ...store.callbacks);
const { promise } = startImport('L3NxFnYoBGjJ5PhxrxV6jorvjnc8cerYJx71vXU6ta8BXQxHVZya', false, false, false, ...store.callbacks);
await promise;
assert.strictEqual(store.state.wallets[0].type, SegwitP2SHWallet.type);
assert.strictEqual(store.state.wallets[0].getAddress(), '3KM9VfdsDf9uT7uwZagoKgVn8z35m9CtSM');
@ -143,7 +167,7 @@ describe('import procedure', () => {
it('can import Bech32 Segwit', async () => {
const store = createStore();
const { promise } = startImport('L1T6FfKpKHi8JE6eBKrsXkenw34d5FfFzJUZ6dLs2utxkSvsDfxZ', false, false, ...store.callbacks);
const { promise } = startImport('L1T6FfKpKHi8JE6eBKrsXkenw34d5FfFzJUZ6dLs2utxkSvsDfxZ', false, false, false, ...store.callbacks);
await promise;
assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type);
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 () => {
const store = createStore();
const { promise } = startImport('L36mabzoQyMZoHHsBFVNB7PUBXgXTynwY6yR7kYZ82EkS7oejVp2', false, false, ...store.callbacks);
const { promise } = startImport('L36mabzoQyMZoHHsBFVNB7PUBXgXTynwY6yR7kYZ82EkS7oejVp2', false, false, false, ...store.callbacks);
await promise;
assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type);
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',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -182,6 +207,7 @@ describe('import procedure', () => {
'abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abaisser abeille',
false,
false,
false,
...store.callbacks,
);
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',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -208,6 +235,7 @@ describe('import procedure', () => {
'eight derive blast guide smoke piece coral burden lottery flower tomato flame',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -221,6 +249,7 @@ describe('import procedure', () => {
'receive happy wash prosper update pet neck acid try profit proud hungry',
true,
false,
false,
...store.callbacks,
);
await promise;
@ -234,6 +263,7 @@ describe('import procedure', () => {
'become salmon motor battle sweet merit romance ecology age squirrel oblige awesome',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -248,6 +278,7 @@ describe('import procedure', () => {
'noble mimic pipe merry knife screen enter dune crop bonus slice card',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -262,6 +293,7 @@ describe('import procedure', () => {
'bitter grass shiver impose acquire brush forget axis eager alone wine silver',
true,
false,
false,
...store.callbacks,
);
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',
false,
false,
false,
...store.callbacks,
);
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',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -297,14 +331,14 @@ describe('import procedure', () => {
const store = createStore();
const tempWallet = new HDSegwitBech32Wallet();
await tempWallet.generate();
const { promise } = startImport(tempWallet.getSecret(), false, false, ...store.callbacks);
const { promise } = startImport(tempWallet.getSecret(), false, false, false, ...store.callbacks);
await promise;
assert.strictEqual(store.state.wallets[0].type, HDSegwitBech32Wallet.type);
});
it('can import Legacy with uncompressed pubkey', async () => {
const store = createStore();
const { promise } = startImport('5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS', false, false, ...store.callbacks);
const { promise } = startImport('5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS', false, false, false, ...store.callbacks);
await promise;
assert.strictEqual(store.state.wallets[0].getSecret(), '5KE6tf9vhYkzYSbgEL6M7xvkY69GMFHF3WxzYaCFMvwMxn3QgRS');
assert.strictEqual(store.state.wallets[0].type, LegacyWallet.type);
@ -313,7 +347,7 @@ describe('import procedure', () => {
it('can import BIP38 encrypted backup', async () => {
const store = createStore('qwerty');
const { promise } = startImport('6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN', false, false, ...store.callbacks);
const { promise } = startImport('6PnU5voARjBBykwSddwCdcn6Eu9EcsK24Gs5zWxbJbPZYW7eiYQP8XgKbN', false, false, false, ...store.callbacks);
await promise;
assert.strictEqual(store.state.wallets[0].getSecret(), 'KxqRtpd9vFju297ACPKHrGkgXuberTveZPXbRDiQ3MXZycSQYtjc');
assert.strictEqual(store.state.wallets[0].type, SegwitBech32Wallet.type);
@ -328,17 +362,17 @@ describe('import procedure', () => {
it('can import watch-only address', async () => {
const store1 = createStore();
const { promise: promise1 } = startImport('1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78', false, false, ...store1.callbacks);
const { promise: promise1 } = startImport('1AhcdMCzby4VXgqrexuMfh7eiSprRFtN78', false, false, false, ...store1.callbacks);
await promise1;
assert.strictEqual(store1.state.wallets[0].type, WatchOnlyWallet.type);
const store2 = createStore();
const { promise: promise2 } = startImport('3EoqYYp7hQSHn5nHqRtWzkgqmK3caQ2SUu', false, false, ...store2.callbacks);
const { promise: promise2 } = startImport('3EoqYYp7hQSHn5nHqRtWzkgqmK3caQ2SUu', false, false, false, ...store2.callbacks);
await promise2;
assert.strictEqual(store2.state.wallets[0].type, WatchOnlyWallet.type);
const store3 = createStore();
const { promise: promise3 } = startImport('bc1q8j4lk4qlhun0n7h5ahfslfldc8zhlxgynfpdj2', false, false, ...store3.callbacks);
const { promise: promise3 } = startImport('bc1q8j4lk4qlhun0n7h5ahfslfldc8zhlxgynfpdj2', false, false, false, ...store3.callbacks);
await promise3;
assert.strictEqual(store3.state.wallets[0].type, WatchOnlyWallet.type);
@ -347,6 +381,7 @@ describe('import procedure', () => {
'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP',
false,
false,
false,
...store4.callbacks,
);
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',
false,
false,
false,
...store.callbacks,
);
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',
true,
false,
false,
...store.callbacks,
);
await promise;
@ -394,6 +431,7 @@ describe('import procedure', () => {
'{"ExtPubKey":"zpub6riZchHnrWzhhZ3Z4dhCJmesGyafMmZBRC9txhnidR313XJbcv4KiDubderKHhL7rMsqacYd82FQ38e4whgs8Dg7CpsxX3dSGWayXsEerF4","MasterFingerprint":"7D2F0272","AccountKeyPath":"84\'\\/0\'\\/0\'","CoboVaultFirmwareVersion":"2.6.1(BTC-Only)"}',
false,
false,
false,
...store.callbacks,
);
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'"}]`,
false,
false,
false,
...store.callbacks,
);
await promise;
@ -442,6 +481,7 @@ describe('import procedure', () => {
'{"ExtPubKey":"zpub6qT7amLcp2exr4mU4AhXZMjD9CFkopECVhUxc9LHW8pNsJG2B9ogs5sFbGZpxEeT5TBjLmc7EFYgZA9EeWEM1xkJMFLefzZc8eigRFhKB8Q","MasterFingerprint":"01EBDA7D","AccountKeyPath":"m/84\'/0\'/0\'"}',
false,
false,
false,
...store.callbacks,
);
await promise;
@ -456,6 +496,7 @@ describe('import procedure', () => {
'trip ener cloc puls hams ghos inha crow inju vibr seve chro',
false,
false,
false,
...store1.callbacks,
);
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',
false,
false,
false,
...store2.callbacks,
);
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',
false,
false,
false,
...store3.callbacks,
);
await promise3;
@ -500,7 +543,7 @@ describe('import procedure', () => {
}
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;
assert.strictEqual(store.state.wallets[0].type, HDLegacyP2PKHWallet.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'),
false,
false,
false,
...store.callbacks,
);
await promise;
@ -530,6 +574,7 @@ describe('import procedure', () => {
fs.readFileSync('tests/unit/fixtures/coldcardmk4/new-wasabi.json').toString('utf8'),
false,
false,
false,
...store.callbacks,
);
await promise;
@ -547,6 +592,7 @@ describe('import procedure', () => {
fs.readFileSync('tests/unit/fixtures/coldcardmk4/sparrow-export.json').toString('utf8'),
false,
false,
false,
...store.callbacks,
);
await promise;