BlueWallet/screen/wallets/ImportCustomDerivationPath.tsx
GLaDOS 7a5589eb00
Some checks are pending
Build Release and Upload to TestFlight (iOS) / build (push) Waiting to run
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Blocked by required conditions
BuildReleaseApk / buildReleaseApk (push) Waiting to run
BuildReleaseApk / browserstack (push) Blocked by required conditions
Merge pull request #8564 from BlueWallet/components
REF: split BlueComponents, prune dead/optional props
2026-05-19 22:16:24 +01:00

230 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { FlatList, StyleSheet, TextInput, View } from 'react-native';
import debounce from '../../blue_modules/debounce';
import BlueFormLabel from '../../components/BlueFormLabel';
import BlueTextCentered from '../../components/BlueTextCentered';
import { HDLegacyP2PKHWallet } from '../../class/wallets/hd-legacy-p2pkh-wallet';
import { HDSegwitBech32Wallet } from '../../class/wallets/hd-segwit-bech32-wallet';
import { HDSegwitP2SHWallet } from '../../class/wallets/hd-segwit-p2sh-wallet';
import { HDTaprootWallet } from '../../class/wallets/hd-taproot-wallet';
import { validateBip32 } from '../../class/wallet-import';
import { TWallet } from '../../class/wallets/types';
import Button from '../../components/Button';
import SafeArea from '../../components/SafeArea';
import { useTheme } from '../../components/themes';
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';
import { BlueSpacing20 } from '../../components/BlueSpacing';
type RouteProps = RouteProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>;
type NavigationProp = NativeStackNavigationProp<AddWalletStackParamList, 'ImportCustomDerivationPath'>;
const ListEmptyComponent: React.FC = () => <BlueTextCentered>{loc.wallets.import_wrong_path}</BlueTextCentered>;
const WRONG_PATH = 'WRONG_PATH';
enum STATUS {
WALLET_FOUND = 'WALLET_FOUND',
WALLET_NOTFOUND = 'WALLET_NOTFOUND',
WALLET_UNKNOWN = 'WALLET_UNKNOWN',
}
type TWalletsByType = { [type: string]: TWallet };
type TWalletsByPath = { [path: string]: TWalletsByType | 'WRONG_PATH' };
type TUsedByType = { [type: string]: STATUS };
type TUsedByPath = { [path: string]: TUsedByType };
type TItem = [type: string, typeReadable: string, STATUS | undefined];
const ImportCustomDerivationPath: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const { colors } = useTheme();
const { importText, password } = useRoute<RouteProps>().params;
const { addAndSaveWallet } = useStorage();
const [path, setPath] = useState<string>("m/84'/0'/0'");
const [wallets, setWallets] = useState<TWalletsByPath>({});
const [used, setUsed] = useState<TUsedByPath>({});
const [selected, setSelected] = useState<string>('');
const importing = useRef(false);
const { isElectrumDisabled } = useSettings();
const debouncedSavePath = useRef(
debounce(async newPath => {
if (!validateBip32(newPath)) {
setWallets(ws => ({ ...ws, [newPath]: WRONG_PATH }));
return;
}
// create wallets
const newWallets: { [type: string]: TWallet } = {};
for (const Wallet of [HDLegacyP2PKHWallet, HDSegwitP2SHWallet, HDSegwitBech32Wallet, HDTaprootWallet]) {
const wallet = new Wallet();
wallet.setSecret(importText);
if (password) {
wallet.setPassphrase(password);
}
wallet.setDerivationPath(newPath);
newWallets[Wallet.type] = wallet;
}
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 => {
const status = v ? STATUS.WALLET_FOUND : STATUS.WALLET_NOTFOUND;
setUsed(u => ({ ...u, [newPath]: { ...u[newPath], [w.type]: status } }));
});
});
try {
await Promise.all(promises);
} catch (e) {
Object.values(newWallets).forEach(w => {
setUsed(u => ({ ...u, [newPath]: { ...u[newPath], [w.type]: STATUS.WALLET_UNKNOWN } }));
});
}
}, 500),
);
useEffect(() => {
if (path in wallets) return;
debouncedSavePath.current(path);
}, [path, wallets]);
const items: TItem[] = useMemo(() => {
if (wallets[path] === WRONG_PATH) return [];
return [
[HDLegacyP2PKHWallet.type, HDLegacyP2PKHWallet.typeReadable, used[path]?.[HDLegacyP2PKHWallet.type]],
[HDSegwitP2SHWallet.type, HDSegwitP2SHWallet.typeReadable, used[path]?.[HDSegwitP2SHWallet.type]],
[HDSegwitBech32Wallet.type, HDSegwitBech32Wallet.typeReadable, used[path]?.[HDSegwitBech32Wallet.type]],
[HDTaprootWallet.type, HDTaprootWallet.typeReadable, used[path]?.[HDTaprootWallet.type]],
];
}, [path, used, wallets]);
const stylesHook = StyleSheet.create({
root: {
backgroundColor: colors.elevated,
},
center: {
backgroundColor: colors.elevated,
},
pathInput: {
borderColor: colors.formBorder,
borderBottomColor: colors.formBorder,
backgroundColor: colors.inputBackgroundColor,
},
});
const saveWallet = (type: string) => {
if (importing.current) return;
importing.current = true;
if (wallets[path] === WRONG_PATH) return;
addAndSaveWallet(wallets[path][type]);
navigation.getParent()?.goBack();
};
const renderItem = ({ item }: { item: TItem }) => {
const [type, title, found] = item;
let subtitle;
switch (found) {
case STATUS.WALLET_FOUND:
subtitle = loc.wallets.import_derivation_found;
break;
case STATUS.WALLET_NOTFOUND:
subtitle = loc.wallets.import_derivation_found_not;
break;
case STATUS.WALLET_UNKNOWN:
subtitle = loc.wallets.import_derivation_unknown;
break;
default:
subtitle = loc.wallets.import_derivation_loading;
}
return <WalletToImport key={type} title={title} subtitle={subtitle} active={selected === type} onPress={() => setSelected(type)} />;
};
const disabled = wallets[path] === WRONG_PATH || wallets[path]?.[selected] === undefined;
// iOS smart punctuation turns ' into and " into ”. Normalize back to ASCII so derivation paths parse.
const handlePathChange = useCallback((text: string) => {
const normalized = text.split('').join("'").split('').join("'").split('“').join('"').split('”').join('"');
setPath(normalized);
}, []);
return (
<SafeArea style={[styles.root, stylesHook.root]}>
<BlueSpacing20 />
<BlueFormLabel>{loc.wallets.import_derivation_subtitle}</BlueFormLabel>
<BlueSpacing20 />
<TextInput
testID="DerivationPathInput"
autoCapitalize="none"
autoCorrect={false}
spellCheck={false}
keyboardType="ascii-capable"
placeholder={loc.send.details_note_placeholder}
value={path}
placeholderTextColor="#81868e"
style={[styles.pathInput, stylesHook.pathInput]}
onChangeText={handlePathChange}
/>
<FlatList
data={items}
keyExtractor={w => path + w[0]}
renderItem={renderItem}
contentContainerStyle={styles.flatListContainer}
ListEmptyComponent={ListEmptyComponent}
/>
<View style={[styles.center, stylesHook.center]}>
<View style={styles.buttonContainer}>
<Button disabled={disabled} title={loc.wallets.import_do_import} testID="ImportButton" onPress={() => saveWallet(selected)} />
</View>
</View>
</SafeArea>
);
};
const styles = StyleSheet.create({
root: {
paddingTop: 10,
},
flatListContainer: {
marginHorizontal: 16,
},
center: {
marginHorizontal: 16,
alignItems: 'center',
top: -100,
},
buttonContainer: {
height: 45,
},
pathInput: {
flexDirection: 'row',
borderWidth: 1,
borderBottomWidth: 0.5,
marginHorizontal: 16,
minHeight: 44,
height: 44,
alignItems: 'center',
marginVertical: 8,
borderRadius: 4,
paddingHorizontal: 8,
color: '#81868e',
},
});
export default ImportCustomDerivationPath;