import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Alert, I18nManager, InteractionManager, LayoutAnimation, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View, } from 'react-native'; import { writeFileAndExport } from '../../blue_modules/fs'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents'; import { HDAezeedWallet, HDSegwitBech32Wallet, LegacyWallet, MultisigHDWallet, SegwitBech32Wallet, SegwitP2SHWallet, WatchOnlyWallet, } from '../../class'; import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet'; import { LightningCustodianWallet } from '../../class/wallets/lightning-custodian-wallet'; import presentAlert from '../../components/Alert'; import Button from '../../components/Button'; import ListItem from '../../components/ListItem'; import SaveFileButton from '../../components/SaveFileButton'; import { SecondButton } from '../../components/SecondButton'; import { useTheme } from '../../components/themes'; import prompt from '../../helpers/prompt'; import { unlockWithBiometrics, useBiometrics } from '../../hooks/useBiometrics'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc, { formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import { useStorage } from '../../hooks/context/useStorage'; import { popToTop } from '../../NavigationService'; import { useFocusEffect, useRoute, RouteProp } from '@react-navigation/native'; import { LightningTransaction, Transaction, TWallet } from '../../class/wallets/types'; import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList'; import { unsubscribe } from '../../blue_modules/notifications'; type RouteProps = RouteProp; const WalletDetails: React.FC = () => { const { saveToDisk, wallets, deleteWallet, setSelectedWalletID, txMetadata } = useStorage(); const { isBiometricUseCapableAndEnabled } = useBiometrics(); const { walletID } = useRoute().params; const [isLoading, setIsLoading] = useState(false); const [backdoorPressed, setBackdoorPressed] = useState(0); const walletRef = useRef(wallets.find(w => w.getID() === walletID)); const wallet = walletRef.current as TWallet; const [walletUseWithHardwareWallet, setWalletUseWithHardwareWallet] = useState( wallet.useWithHardwareWalletEnabled ? wallet.useWithHardwareWalletEnabled() : false, ); const [isBIP47Enabled, setIsBIP47Enabled] = useState(wallet.isBIP47Enabled ? wallet.isBIP47Enabled() : false); const [isContactsVisible, setIsContactsVisible] = useState( (wallet.allowBIP47 && wallet.allowBIP47() && wallet.isBIP47Enabled && wallet.isBIP47Enabled()) || false, ); const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState( wallet.getHideTransactionsInWalletsList ? !wallet.getHideTransactionsInWalletsList() : true, ); const { setOptions, navigate, addListener } = useExtendedNavigation(); const { colors } = useTheme(); const [walletName, setWalletName] = useState(wallet.getLabel()); const [masterFingerprint, setMasterFingerprint] = useState(); const walletTransactionsLength = useMemo(() => wallet.getTransactions().length, [wallet]); const derivationPath = useMemo(() => { try { // @ts-expect-error: Need to fix later if (wallet.getDerivationPath) { // @ts-expect-error: Need to fix later const path = wallet.getDerivationPath(); return path.length > 0 ? path : null; } return null; } catch (e) { return null; } }, [wallet]); const [isToolTipMenuVisible, setIsToolTipMenuVisible] = useState(false); const [isMasterFingerPrintVisible, setIsMasterFingerPrintVisible] = useState(false); const onMenuWillShow = () => setIsToolTipMenuVisible(true); const onMenuWillHide = () => setIsToolTipMenuVisible(false); useEffect(() => { setIsContactsVisible(wallet.allowBIP47 && wallet.allowBIP47() && isBIP47Enabled); }, [isBIP47Enabled, wallet]); useFocusEffect( useCallback(() => { const task = InteractionManager.runAfterInteractions(() => { if (isMasterFingerPrintVisible && wallet.allowMasterFingerprint && wallet.allowMasterFingerprint()) { // @ts-expect-error: Need to fix later if (wallet.getMasterFingerprintHex) { // @ts-expect-error: Need to fix later setMasterFingerprint(wallet.getMasterFingerprintHex()); } } else { setMasterFingerprint(undefined); } }); return () => task.cancel(); }, [isMasterFingerPrintVisible, wallet]), ); const stylesHook = StyleSheet.create({ textLabel1: { color: colors.feeText, }, textLabel2: { color: colors.feeText, }, textValue: { color: colors.outputValue, }, input: { borderColor: colors.formBorder, borderBottomColor: colors.formBorder, backgroundColor: colors.inputBackgroundColor, }, delete: { color: isToolTipMenuVisible ? colors.buttonDisabledTextColor : '#d0021b', }, }); useEffect(() => { setOptions({ headerBackTitleVisible: true, }); }, [setOptions]); useEffect(() => { if (wallets.some(w => w.getID() === walletID)) { setSelectedWalletID(walletID); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletID]); const navigateToOverviewAndDeleteWallet = useCallback(async () => { setIsLoading(true); try { const externalAddresses = wallet.getAllExternalAddresses(); if (externalAddresses.length > 0) { await unsubscribe(externalAddresses, [], []); } deleteWallet(wallet); saveToDisk(true); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); popToTop(); } catch (e: unknown) { console.error(e); triggerHapticFeedback(HapticFeedbackTypes.NotificationError); presentAlert({ message: (e as Error).message }); setIsLoading(false); } }, [deleteWallet, saveToDisk, wallet]); const presentWalletHasBalanceAlert = useCallback(async () => { triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning); try { const balance = formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, true); const walletBalanceConfirmation = await prompt( loc.wallets.details_delete_wallet, loc.formatString(loc.wallets.details_del_wb_q, { balance }), true, 'numeric', true, loc.wallets.details_delete, ); // Remove any non-numeric characters before comparison const cleanedConfirmation = (walletBalanceConfirmation || '').replace(/[^0-9]/g, ''); if (Number(cleanedConfirmation) === wallet.getBalance()) { await navigateToOverviewAndDeleteWallet(); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); } else { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); setIsLoading(false); presentAlert({ message: loc.wallets.details_del_wb_err }); } } catch (_) {} }, [navigateToOverviewAndDeleteWallet, wallet]); const navigateToWalletExport = () => { navigate('WalletExportRoot', { screen: 'WalletExport', params: { walletID, }, }); }; const navigateToMultisigCoordinationSetup = () => { navigate('ExportMultisigCoordinationSetupRoot', { screen: 'ExportMultisigCoordinationSetup', params: { walletID, }, }); }; const navigateToViewEditCosigners = () => { navigate('ViewEditMultisigCosignersRoot', { screen: 'ViewEditMultisigCosigners', params: { walletID, }, }); }; const navigateToXPub = () => navigate('WalletXpubRoot', { screen: 'WalletXpub', params: { walletID, }, }); const navigateToSignVerify = () => navigate('SignVerifyRoot', { screen: 'SignVerify', params: { walletID, address: wallet.getAllExternalAddresses()[0], // works for both single address and HD wallets }, }); const navigateToAddresses = () => navigate('WalletAddresses', { walletID, }); const navigateToContacts = () => navigate('PaymentCodeList', { walletID }); const exportInternals = async () => { if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1); setBackdoorPressed(0); if (wallet.type !== HDSegwitBech32Wallet.type) return; const fileName = 'wallet-externals.json'; const contents = JSON.stringify( { _balances_by_external_index: wallet._balances_by_external_index, _balances_by_internal_index: wallet._balances_by_internal_index, _txs_by_external_index: wallet._txs_by_external_index, _txs_by_internal_index: wallet._txs_by_internal_index, _utxo: wallet._utxo, next_free_address_index: wallet.next_free_address_index, next_free_change_address_index: wallet.next_free_change_address_index, internal_addresses_cache: wallet.internal_addresses_cache, external_addresses_cache: wallet.external_addresses_cache, _xpub: wallet._xpub, gap_limit: wallet.gap_limit, label: wallet.label, _lastTxFetch: wallet._lastTxFetch, _lastBalanceFetch: wallet._lastBalanceFetch, }, null, 2, ); await writeFileAndExport(fileName, contents, false); }; const purgeTransactions = async () => { if (backdoorPressed < 10) return setBackdoorPressed(backdoorPressed + 1); setBackdoorPressed(0); const msg = 'Transactions purged. Pls go to main screen and back to rerender screen'; if (wallet.type === HDSegwitBech32Wallet.type) { wallet._txs_by_external_index = {}; wallet._txs_by_internal_index = {}; presentAlert({ message: msg }); } // @ts-expect-error: Need to fix later if (wallet._hdWalletInstance) { // @ts-expect-error: Need to fix later wallet._hdWalletInstance._txs_by_external_index = {}; // @ts-expect-error: Need to fix later wallet._hdWalletInstance._txs_by_internal_index = {}; presentAlert({ message: msg }); } }; const walletNameTextInputOnBlur = useCallback(async () => { const trimmedWalletName = walletName.trim(); if (trimmedWalletName.length === 0) { const walletLabel = wallet.getLabel(); setWalletName(walletLabel); } else if (wallet.getLabel() !== trimmedWalletName) { // Only save if the name has changed wallet.setLabel(trimmedWalletName); try { console.warn('saving wallet name:', trimmedWalletName); await saveToDisk(); } catch (error) { console.error((error as Error).message); } } }, [wallet, walletName, saveToDisk]); useEffect(() => { const subscribe = addListener('beforeRemove', () => { walletNameTextInputOnBlur(); }); return subscribe; }, [addListener, walletName, walletNameTextInputOnBlur]); const exportHistoryContent = useCallback(() => { const headers = [loc.transactions.date, loc.transactions.txid, `${loc.send.create_amount} (${BitcoinUnit.BTC})`, loc.send.create_memo]; if (wallet.chain === Chain.OFFCHAIN) { headers.push(loc.lnd.payment); } const rows = [headers.join(',')]; const transactions = wallet.getTransactions(); transactions.forEach((transaction: Transaction & LightningTransaction) => { const value = formatBalanceWithoutSuffix(transaction.value || 0, BitcoinUnit.BTC, true); let hash: string = transaction.hash || ''; let memo = (transaction.hash && txMetadata[transaction.hash]?.memo?.trim()) || ''; let status = ''; if (wallet.chain === Chain.OFFCHAIN) { hash = transaction.payment_hash ? transaction.payment_hash.toString() : ''; memo = transaction.memo || ''; status = transaction.ispaid ? loc._.success : loc.lnd.expired; if (typeof hash !== 'string' && (hash as any)?.type === 'Buffer' && (hash as any)?.data) { hash = Buffer.from((hash as any).data).toString('hex'); } } const date = transaction.received ? new Date(transaction.received).toString() : ''; const data = [date, hash, value, memo]; if (wallet.chain === Chain.OFFCHAIN) { data.push(status); } rows.push(data.join(',')); }); return rows.join('\n'); }, [wallet, txMetadata]); const handleDeleteButtonTapped = () => { triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning); Alert.alert( loc.wallets.details_delete_wallet, loc.wallets.details_are_you_sure, [ { text: loc.wallets.details_yes_delete, onPress: async () => { const isBiometricsEnabled = await isBiometricUseCapableAndEnabled(); if (isBiometricsEnabled) { if (!(await unlockWithBiometrics())) { return; } } if (wallet.getBalance && wallet.getBalance() > 0 && wallet.allowSend && wallet.allowSend()) { presentWalletHasBalanceAlert(); } else { navigateToOverviewAndDeleteWallet(); } }, style: 'destructive', }, { text: loc.wallets.details_no_cancel, onPress: () => {}, style: 'cancel' }, ], { cancelable: false }, ); }; const fileName = useMemo(() => { const label = wallet.getLabel().replace(' ', '-'); return `${label}-history.csv`; }, [wallet]); const onViewMasterFingerPrintPress = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setIsMasterFingerPrintVisible(true); }; return ( {isLoading ? ( ) : ( {(() => { if ( [LegacyWallet.type, SegwitBech32Wallet.type, SegwitP2SHWallet.type].includes(wallet.type) || (wallet.type === WatchOnlyWallet.type && !wallet.isHd()) ) { return ( <> {loc.wallets.details_address.toLowerCase()} {(() => { // gracefully handling faulty wallets, so at least user has an option to delete the wallet try { return wallet.getAddress ? wallet.getAddress() : ''; } catch (error: any) { return error.message; } })()} ); } })()} {loc.wallets.add_wallet_name.toLowerCase()} { setWalletName(text); }} onChange={event => { const text = event.nativeEvent.text; setWalletName(text); }} onBlur={walletNameTextInputOnBlur} numberOfLines={1} placeholderTextColor="#81868e" style={styles.inputText} editable={!isLoading} underlineColorAndroid="transparent" testID="WalletNameInput" /> {loc.wallets.details_type.toLowerCase()} {wallet.typeReadable} {wallet.type === MultisigHDWallet.type && ( <> {loc.wallets.details_multisig_type} {`${wallet.getM()} / ${wallet.getN()} (${ wallet.isNativeSegwit() ? 'native segwit' : wallet.isWrappedSegwit() ? 'wrapped segwit' : 'legacy' })`} )} {wallet.type === MultisigHDWallet.type && ( <> {loc.multisig.how_many_signatures_can_bluewallet_make} {wallet.howManySignaturesCanWeMake()} )} {wallet.type === LightningCustodianWallet.type && ( <> {loc.wallets.details_connected_to.toLowerCase()} {wallet.getBaseURI()} )} {wallet.type === HDAezeedWallet.type && ( <> {loc.wallets.identity_pubkey.toLowerCase()} {wallet.getIdentityPubkey()} )} <> {loc.transactions.list_title.toLowerCase()} {loc.wallets.details_display} { if (wallet.setHideTransactionsInWalletsList) { wallet.setHideTransactionsInWalletsList(!value); triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); setHideTransactionsInWalletsList(!wallet.getHideTransactionsInWalletsList()); } try { await saveToDisk(); } catch (error: any) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); console.error(error.message); } }} /> <> {loc.transactions.transactions_count.toLowerCase()} {wallet.getTransactions().length} {wallet.allowBIP47 && wallet.allowBIP47() ? ( <> {loc.bip47.payment_code} {loc.bip47.purpose} { setIsBIP47Enabled(value); if (wallet.switchBIP47) { wallet.switchBIP47(value); triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); } try { await saveToDisk(); } catch (error: unknown) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); console.error((error as Error).message); } }} testID="BIP47Switch" /> ) : null} {wallet.type === WatchOnlyWallet.type && wallet.isHd && wallet.isHd() && ( <> {loc.wallets.details_advanced.toLowerCase()} {loc.wallets.details_use_with_hardware_wallet} { setWalletUseWithHardwareWallet(value); if (wallet.setUseWithHardwareWalletEnabled) { wallet.setUseWithHardwareWalletEnabled(value); triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); } try { await saveToDisk(); } catch (error: unknown) { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); console.error((error as Error).message); } }} /> )} {wallet.allowMasterFingerprint && wallet.allowMasterFingerprint() && ( {loc.wallets.details_master_fingerprint.toLowerCase()} {isMasterFingerPrintVisible ? ( {masterFingerprint ?? } ) : ( {loc.multisig.view} )} )} {derivationPath && ( {loc.wallets.details_derivation_path} {derivationPath} )} {(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd && wallet.isHd())) && ( )} {isContactsVisible ? ( ) : null}