import React, { useContext, useRef, useState, useEffect, useCallback } from 'react'; import { ActivityIndicator, FlatList, I18nManager, InteractionManager, Keyboard, KeyboardAvoidingView, LayoutAnimation, Platform, StyleSheet, Switch, Text, TouchableOpacity, View, } from 'react-native'; import { Icon } from 'react-native-elements'; import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; import { BlueButtonLink, BlueFormMultiInput, BlueSpacing10, BlueSpacing20, BlueText, BlueTextCentered } from '../../BlueComponents'; import navigationStyle from '../../components/navigationStyle'; import { HDSegwitBech32Wallet, MultisigCosigner, MultisigHDWallet } from '../../class'; import loc from '../../loc'; import { SquareButton } from '../../components/SquareButton'; import BottomModal from '../../components/BottomModal'; import MultipleStepsListItem, { MultipleStepsListItemButtohType, MultipleStepsListItemDashType, } from '../../components/MultipleStepsListItem'; import { BlueStorageContext } from '../../blue_modules/storage-context'; import { encodeUR } from '../../blue_modules/ur'; import QRCodeComponent from '../../components/QRCodeComponent'; import presentAlert from '../../components/Alert'; import confirm from '../../helpers/confirm'; import { scanQrHelper } from '../../helpers/scan-qr'; import { useTheme } from '../../components/themes'; import Button from '../../components/Button'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import usePrivacy from '../../hooks/usePrivacy'; import prompt from '../../helpers/prompt'; import A from '../../blue_modules/analytics'; import SaveFileButton from '../../components/SaveFileButton'; const staticCache = {}; const WalletsAddMultisigStep2 = () => { const { addWallet, saveToDisk, isElectrumDisabled, isAdvancedModeEnabled, sleep, currentSharedCosigner, setSharedCosigner } = useContext(BlueStorageContext); const { colors } = useTheme(); const navigation = useNavigation(); const { m, n, format, walletLabel } = useRoute().params; const { name } = useRoute(); const [cosigners, setCosigners] = useState([]); // array of cosigners user provided. if format [cosigner, fp, path] const [isLoading, setIsLoading] = useState(false); const [isMnemonicsModalVisible, setIsMnemonicsModalVisible] = useState(false); const [isProvideMnemonicsModalVisible, setIsProvideMnemonicsModalVisible] = useState(false); const [isRenderCosignersXpubModalVisible, setIsRenderCosignersXpubModalVisible] = useState(false); const [cosignerXpub, setCosignerXpub] = useState(''); // string used in exportCosigner() const [cosignerXpubURv2, setCosignerXpubURv2] = useState(''); // string displayed in renderCosignersXpubModal() const [cosignerXpubFilename, setCosignerXpubFilename] = useState('bw-cosigner.bwcosigner'); const [vaultKeyData, setVaultKeyData] = useState({ keyIndex: 1, xpub: '', seed: '', isLoading: false }); // string rendered in modal const [importText, setImportText] = useState(''); const [askPassphrase, setAskPassphrase] = useState(false); const [isAdvancedModeEnabledRender, setIsAdvancedModeEnabledRender] = useState(false); const openScannerButton = useRef(); const data = useRef(new Array(n)); const { enableBlur, disableBlur } = usePrivacy(); useEffect(() => { isAdvancedModeEnabled().then(setIsAdvancedModeEnabledRender); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useFocusEffect( useCallback(() => { enableBlur(); return () => { disableBlur(); }; }, [disableBlur, enableBlur]), ); useEffect(() => { console.log(currentSharedCosigner); if (currentSharedCosigner) { (async function () { if (await confirm(loc.multisig.shared_key_detected, loc.multisig.shared_key_detected_question)) { setImportText(currentSharedCosigner); setIsProvideMnemonicsModalVisible(true); setSharedCosigner(''); } })(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentSharedCosigner]); const handleOnHelpPress = () => { navigation.navigate('WalletsAddMultisigHelp'); }; const stylesHook = StyleSheet.create({ root: { backgroundColor: colors.elevated, }, textDestination: { color: colors.foregroundColor, }, modalContent: { backgroundColor: colors.modal, }, exportButton: { backgroundColor: colors.buttonDisabledBackgroundColor, }, vaultKeyText: { color: colors.alternativeTextColor, }, vaultKeyCircleSuccess: { backgroundColor: colors.msSuccessBG, }, word: { backgroundColor: colors.inputBackgroundColor, }, wordText: { color: colors.labelText, }, helpButton: { backgroundColor: colors.buttonDisabledBackgroundColor, }, helpButtonText: { color: colors.foregroundColor, }, }); const onCreate = async () => { setIsLoading(true); await sleep(100); try { await _onCreate(); // this can fail with "Duplicate fingerprint" error or other } catch (e) { setIsLoading(false); presentAlert({ message: e.message }); console.log('create MS wallet error', e); } }; const _onCreate = async () => { const w = new MultisigHDWallet(); w.setM(m); switch (format) { case MultisigHDWallet.FORMAT_P2WSH: w.setNativeSegwit(); w.setDerivationPath(MultisigHDWallet.PATH_NATIVE_SEGWIT); break; case MultisigHDWallet.FORMAT_P2SH_P2WSH: case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: w.setWrappedSegwit(); w.setDerivationPath(MultisigHDWallet.PATH_WRAPPED_SEGWIT); break; case MultisigHDWallet.FORMAT_P2SH: w.setLegacy(); w.setDerivationPath(MultisigHDWallet.PATH_LEGACY); break; default: throw new Error('This should never happen'); } for (const cc of cosigners) { const fp = cc[1] || getFpCacheForMnemonics(cc[0], cc[3]); w.addCosigner(cc[0], fp, cc[2], cc[3]); } w.setLabel(walletLabel); if (!isElectrumDisabled) { await w.fetchBalance(); } addWallet(w); await saveToDisk(); A(A.ENUM.CREATED_WALLET); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); navigation.getParent().goBack(); }; const generateNewKey = () => { const w = new HDSegwitBech32Wallet(); w.generate().then(() => { const cosignersCopy = [...cosigners]; cosignersCopy.push([w.getSecret(), false, false]); if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setCosigners(cosignersCopy); setVaultKeyData({ keyIndex: cosignersCopy.length, seed: w.getSecret(), xpub: w.getXpub(), isLoading: false }); setIsLoading(true); setIsMnemonicsModalVisible(true); // filling cache setTimeout(() => { // filling cache setXpubCacheForMnemonics(w.getSecret()); setFpCacheForMnemonics(w.getSecret()); setIsLoading(false); }, 500); }); }; const getPath = () => { let path = ''; switch (format) { case MultisigHDWallet.FORMAT_P2WSH: path = MultisigHDWallet.PATH_NATIVE_SEGWIT; break; case MultisigHDWallet.FORMAT_P2SH_P2WSH: case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: path = MultisigHDWallet.PATH_WRAPPED_SEGWIT; break; case MultisigHDWallet.FORMAT_P2SH: path = MultisigHDWallet.PATH_LEGACY; break; default: throw new Error('This should never happen'); } return path; }; const viewKey = cosigner => { if (MultisigHDWallet.isXpubValid(cosigner[0])) { setCosignerXpub(MultisigCosigner.exportToJson(cosigner[1], cosigner[0], cosigner[2])); setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(cosigner[1], cosigner[0], cosigner[2]))[0]); setCosignerXpubFilename('bw-cosigner-' + cosigner[1] + '.bwcosigner'); setIsRenderCosignersXpubModalVisible(true); } else { const path = getPath(); const xpub = getXpubCacheForMnemonics(cosigner[0]); const fp = getFpCacheForMnemonics(cosigner[0], cosigner[3]); setCosignerXpub(MultisigCosigner.exportToJson(fp, xpub, path)); setCosignerXpubURv2(encodeUR(MultisigCosigner.exportToJson(fp, xpub, path))[0]); setCosignerXpubFilename('bw-cosigner-' + fp + '.bwcosigner'); setIsRenderCosignersXpubModalVisible(true); } }; const getXpubCacheForMnemonics = seed => { const path = getPath(); return staticCache[seed + path] || setXpubCacheForMnemonics(seed); }; const setXpubCacheForMnemonics = seed => { const path = getPath(); const w = new MultisigHDWallet(); w.setDerivationPath(path); staticCache[seed + path] = w.convertXpubToMultisignatureXpub(MultisigHDWallet.seedToXpub(seed, path)); return staticCache[seed + path]; }; const getFpCacheForMnemonics = (seed, passphrase) => { return staticCache[seed + (passphrase ?? '')] || setFpCacheForMnemonics(seed, passphrase); }; const setFpCacheForMnemonics = (seed, passphrase) => { staticCache[seed + (passphrase ?? '')] = MultisigHDWallet.mnemonicToFingerprint(seed, passphrase); return staticCache[seed + (passphrase ?? '')]; }; const iHaveMnemonics = () => { setIsProvideMnemonicsModalVisible(true); }; const tryUsingXpub = async (xpub, fp, path) => { if (!MultisigHDWallet.isXpubForMultisig(xpub)) { setIsProvideMnemonicsModalVisible(false); setIsLoading(false); setImportText(''); setAskPassphrase(false); presentAlert({ message: loc.multisig.not_a_multisignature_xpub }); return; } if (fp) { // do nothing, it's already set } else { try { fp = await prompt(loc.multisig.input_fp, loc.multisig.input_fp_explain, true, 'plain-text'); fp = (fp + '').toUpperCase(); if (!MultisigHDWallet.isFpValid(fp)) fp = '00000000'; } catch (e) { return setIsLoading(false); } } if (path) { // do nothing, it's already set } else { try { path = await prompt( loc.multisig.input_path, loc.formatString(loc.multisig.input_path_explain, { default: getPath() }), true, 'plain-text', ); if (!MultisigHDWallet.isPathValid(path)) path = getPath(); } catch { return setIsLoading(false); } } setIsProvideMnemonicsModalVisible(false); setIsLoading(false); setImportText(''); setAskPassphrase(false); const cosignersCopy = [...cosigners]; cosignersCopy.push([xpub, fp, path]); if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setCosigners(cosignersCopy); }; const useMnemonicPhrase = async () => { setIsLoading(true); if (MultisigHDWallet.isXpubValid(importText)) { return tryUsingXpub(importText); } try { const jsonText = JSON.parse(importText); let fp; let path; if (jsonText.xpub) { if (jsonText.xfp) { fp = jsonText.xfp; } if (jsonText.path) { path = jsonText.path; } return tryUsingXpub(jsonText.xpub, fp, path); } } catch {} const hd = new HDSegwitBech32Wallet(); hd.setSecret(importText); if (!hd.validateMnemonic()) { setIsLoading(false); return presentAlert({ message: loc.multisig.invalid_mnemonics }); } let passphrase; if (askPassphrase) { try { passphrase = await prompt(loc.wallets.import_passphrase_title, loc.wallets.import_passphrase_message); } catch (e) { if (e.message === 'Cancel Pressed') { setIsLoading(false); return; } throw e; } } const cosignersCopy = [...cosigners]; cosignersCopy.push([hd.getSecret(), false, false, passphrase]); if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setCosigners(cosignersCopy); setIsProvideMnemonicsModalVisible(false); setIsLoading(false); setImportText(''); setAskPassphrase(false); }; const isValidMnemonicSeed = mnemonicSeed => { const hd = new HDSegwitBech32Wallet(); hd.setSecret(mnemonicSeed); return hd.validateMnemonic(); }; const onBarScanned = ret => { if (!ret.data) ret = { data: ret }; try { let retData = JSON.parse(ret.data); if (Array.isArray(retData) && retData.length === 1) { // UR:CRYPTO-ACCOUNT now parses as an array of accounts, even if it is just one, // so in case of cosigner data its gona be an array of 1 cosigner account. lets pop it for // the code that expects it retData = retData.pop(); ret.data = JSON.stringify(retData); } } catch (_) {} if (ret.data.toUpperCase().startsWith('UR')) { presentAlert({ message: 'BC-UR not decoded. This should never happen' }); } else if (isValidMnemonicSeed(ret.data)) { setIsProvideMnemonicsModalVisible(true); setImportText(ret.data); } else { if (MultisigHDWallet.isXpubValid(ret.data) && !MultisigHDWallet.isXpubForMultisig(ret.data)) { return presentAlert({ message: loc.multisig.not_a_multisignature_xpub }); } if (MultisigHDWallet.isXpubValid(ret.data)) { return tryUsingXpub(ret.data); } let cosigner = new MultisigCosigner(ret.data); if (!cosigner.isValid()) return presentAlert({ message: loc.multisig.invalid_cosigner }); setIsProvideMnemonicsModalVisible(false); if (cosigner.howManyCosignersWeHave() > 1) { // lets look for the correct cosigner. thats probably gona be the one with specific corresponding path, // for example m/48'/0'/0'/2' if user chose to setup native segwit in BW for (const cc of cosigner.getAllCosigners()) { switch (format) { case MultisigHDWallet.FORMAT_P2WSH: if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/2'")) { // found it cosigner = cc; } break; case MultisigHDWallet.FORMAT_P2SH_P2WSH: case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: if (cc.getPath().startsWith('m/48') && cc.getPath().endsWith("/1'")) { // found it cosigner = cc; } break; case MultisigHDWallet.FORMAT_P2SH: if (cc.getPath().startsWith('m/45')) { // found it cosigner = cc; } break; default: throw new Error('This should never happen'); } } } for (const existingCosigner of cosigners) { if (existingCosigner[0] === cosigner.getXpub()) return presentAlert({ message: loc.multisig.this_cosigner_is_already_imported }); } // now, validating that cosigner is in correct format: let correctFormat = false; switch (format) { case MultisigHDWallet.FORMAT_P2WSH: if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/2'")) { correctFormat = true; } break; case MultisigHDWallet.FORMAT_P2SH_P2WSH: case MultisigHDWallet.FORMAT_P2SH_P2WSH_ALT: if (cosigner.getPath().startsWith('m/48') && cosigner.getPath().endsWith("/1'")) { correctFormat = true; } break; case MultisigHDWallet.FORMAT_P2SH: if (cosigner.getPath().startsWith('m/45')) { correctFormat = true; } break; default: throw new Error('This should never happen'); } if (!correctFormat) return presentAlert({ message: loc.formatString(loc.multisig.invalid_cosigner_format, { format }) }); const cosignersCopy = [...cosigners]; cosignersCopy.push([cosigner.getXpub(), cosigner.getFp(), cosigner.getPath()]); if (Platform.OS !== 'android') LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setCosigners(cosignersCopy); } }; const scanOrOpenFile = () => { setIsProvideMnemonicsModalVisible(false); InteractionManager.runAfterInteractions(async () => { const scanned = await scanQrHelper(navigation.navigate, name, true); onBarScanned({ data: scanned }); }); }; const dashType = ({ index, lastIndex, isChecked, isFocus }) => { if (isChecked) { if (index === lastIndex) { return MultipleStepsListItemDashType.top; } else { return MultipleStepsListItemDashType.topAndBottom; } } else { if (index === lastIndex) { return isFocus ? MultipleStepsListItemDashType.topAndBottom : MultipleStepsListItemDashType.top; } else { return MultipleStepsListItemDashType.topAndBottom; } } }; const _renderKeyItem = el => { const renderProvideKeyButtons = el.index === cosigners.length; const isChecked = el.index < cosigners.length; return ( { viewKey(cosigners[el.index]); }, }} /> {renderProvideKeyButtons && ( <> { setVaultKeyData({ keyIndex: el.index, xpub: '', seed: '', isLoading: true }); generateNewKey(); }, text: loc.multisig.create_new_key, disabled: vaultKeyData.isLoading, }} dashes={MultipleStepsListItemDashType.topAndBottom} checked={isChecked} /> )} ); }; const renderSecret = entries => { const component = []; const entriesObject = entries.entries(); for (const [index, secret] of entriesObject) { if (entries.length > 1) { const text = `${index + 1}. ${secret} `; component.push( {text} , ); } else { const text = `${secret} `; component.push( {text} , ); } } return component; }; const renderMnemonicsModal = () => { return ( {loc.formatString(loc.multisig.vault_key, { number: vaultKeyData.keyIndex })} {loc.multisig.wallet_key_created} {loc._.seed} {renderSecret(vaultKeyData.seed.split(' '))} {isLoading ? :