import { useRoute } from '@react-navigation/native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Alert, I18nManager, InteractionManager, Keyboard, KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native'; import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import Notifications from '../../blue_modules/notifications'; import { useStorage } from '../../blue_modules/storage-context'; import { HDAezeedWallet, HDSegwitBech32Wallet, LegacyWallet, LightningLdkWallet, 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 { SecondButton } from '../../components/SecondButton'; import { useTheme } from '../../components/themes'; import prompt from '../../helpers/prompt'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc, { formatBalanceWithoutSuffix } from '../../loc'; import { BitcoinUnit, Chain } from '../../models/bitcoinUnits'; import SaveFileButton from '../../components/SaveFileButton'; import { useSettings } from '../../components/Context/SettingsContext'; import HeaderRightButton from '../../components/HeaderRightButton'; import { writeFileAndExport } from '../../blue_modules/fs'; import { useBiometrics } from '../../hooks/useBiometrics'; const styles = StyleSheet.create({ scrollViewContent: { flexGrow: 1, }, address: { alignItems: 'center', flex: 1, }, textLabel1: { fontWeight: '500', fontSize: 14, marginVertical: 12, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr', }, textLabel2: { fontWeight: '500', fontSize: 14, marginVertical: 16, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr', }, textValue: { fontWeight: '500', fontSize: 14, }, input: { flexDirection: 'row', borderWidth: 1, borderBottomWidth: 0.5, minHeight: 44, height: 44, alignItems: 'center', borderRadius: 4, }, inputText: { flex: 1, marginHorizontal: 8, minHeight: 33, color: '#81868e', writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr', }, hardware: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, delete: { fontSize: 15, fontWeight: '500', textAlign: 'center', }, row: { flexDirection: 'row', }, marginRight16: { marginRight: 16, }, }); const WalletDetails = () => { const { saveToDisk, wallets, deleteWallet, setSelectedWalletID, txMetadata } = useStorage(); const { isBiometricUseCapableAndEnabled, unlockWithBiometrics } = useBiometrics(); const { walletID } = useRoute().params; const [isLoading, setIsLoading] = useState(false); const [backdoorPressed, setBackdoorPressed] = useState(0); const [backdoorBip47Pressed, setBackdoorBip47Pressed] = useState(0); const wallet = useRef(wallets.find(w => w.getID() === walletID)).current; const [walletName, setWalletName] = useState(wallet.getLabel()); const [useWithHardwareWallet, setUseWithHardwareWallet] = useState(wallet.useWithHardwareWalletEnabled()); const { isAdvancedModeEnabled } = useSettings(); const [isBIP47Enabled, setIsBIP47Enabled] = useState(wallet.isBIP47Enabled()); const [hideTransactionsInWalletsList, setHideTransactionsInWalletsList] = useState(!wallet.getHideTransactionsInWalletsList()); const { goBack, setOptions, popToTop, navigate } = useExtendedNavigation(); const { colors } = useTheme(); const [masterFingerprint, setMasterFingerprint] = useState(); const walletTransactionsLength = useMemo(() => wallet.getTransactions().length, [wallet]); const derivationPath = useMemo(() => { try { const path = wallet.getDerivationPath(); return path.length > 0 ? path : null; } catch (e) { return null; } }, [wallet]); const [lightningWalletInfo, setLightningWalletInfo] = useState({}); const [isToolTipMenuVisible, setIsToolTipMenuVisible] = useState(false); const onMenuWillShow = () => setIsToolTipMenuVisible(true); const onMenuWillHide = () => setIsToolTipMenuVisible(false); useEffect(() => { if (isAdvancedModeEnabled && wallet.allowMasterFingerprint()) { InteractionManager.runAfterInteractions(() => { setMasterFingerprint(wallet.getMasterFingerprintHex()); }); } }, [isAdvancedModeEnabled, 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(() => { if (wallet.type === LightningLdkWallet.type) { wallet.getInfo().then(setLightningWalletInfo); } }, [wallet]); const handleSave = useCallback(() => { setIsLoading(true); if (walletName.trim().length > 0) { wallet.setLabel(walletName.trim()); if (wallet.type === WatchOnlyWallet.type && wallet.isHd()) { wallet.setUseWithHardwareWalletEnabled(useWithHardwareWallet); } wallet.setHideTransactionsInWalletsList(!hideTransactionsInWalletsList); if (wallet.allowBIP47()) { wallet.switchBIP47(isBIP47Enabled); } } saveToDisk() .then(() => { presentAlert({ message: loc.wallets.details_wallet_updated }); goBack(); }) .catch(error => { console.log(error.message); setIsLoading(false); }); }, [walletName, saveToDisk, wallet, hideTransactionsInWalletsList, useWithHardwareWallet, isBIP47Enabled, goBack]); const SaveButton = useMemo( () => , [isLoading, handleSave], ); useEffect(() => { setOptions({ headerRight: () => SaveButton, }); }, [SaveButton, setOptions]); useEffect(() => { if (wallets.some(w => w.getID() === walletID)) { setSelectedWalletID(walletID); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [walletID]); const navigateToOverviewAndDeleteWallet = () => { setIsLoading(true); let externalAddresses = []; try { externalAddresses = wallet.getAllExternalAddresses(); } catch (_) {} Notifications.unsubscribe(externalAddresses, [], []); popToTop(); deleteWallet(wallet); saveToDisk(true); triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); }; const presentWalletHasBalanceAlert = useCallback(async () => { triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning); try { const walletBalanceConfirmation = await prompt( loc.wallets.details_delete_wallet, loc.formatString(loc.wallets.details_del_wb_q, { balance: wallet.getBalance() }), true, 'plain-text', true, loc.wallets.details_delete, ); if (Number(walletBalanceConfirmation) === wallet.getBalance()) { navigateToOverviewAndDeleteWallet(); } else { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); setIsLoading(false); presentAlert({ message: loc.wallets.details_del_wb_err }); } } catch (_) {} // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const navigateToWalletExport = () => { navigate('WalletExportRoot', { screen: 'WalletExport', params: { walletID: wallet.getID(), }, }); }; const navigateToMultisigCoordinationSetup = () => { navigate('ExportMultisigCoordinationSetupRoot', { screen: 'ExportMultisigCoordinationSetup', params: { walletID: wallet.getID(), }, }); }; const navigateToViewEditCosigners = () => { navigate('ViewEditMultisigCosignersRoot', { screen: 'ViewEditMultisigCosigners', params: { walletID, }, }); }; const navigateToXPub = () => navigate('WalletXpubRoot', { screen: 'WalletXpub', params: { walletID, }, }); const navigateToSignVerify = () => navigate('SignVerifyRoot', { screen: 'SignVerify', params: { walletID: wallet.getID(), address: wallet.getAllExternalAddresses()[0], // works for both single address and HD wallets }, }); const navigateToLdkViewLogs = () => { navigate('LdkViewLogs', { walletID, }); }; const navigateToAddresses = () => navigate('WalletAddresses', { walletID: wallet.getID(), }); const navigateToPaymentCodes = () => navigate('PaymentCodeRoot', { screen: 'PaymentCodesList', params: { walletID: wallet.getID(), }, }); 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 }); } if (wallet._hdWalletInstance) { wallet._hdWalletInstance._txs_by_external_index = {}; wallet._hdWalletInstance._txs_by_internal_index = {}; presentAlert({ message: msg }); } }; const walletNameTextInputOnBlur = () => { if (walletName.trim().length === 0) { const walletLabel = wallet.getLabel(); setWalletName(walletLabel); } }; 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 => { const value = formatBalanceWithoutSuffix(transaction.value, BitcoinUnit.BTC, true); let hash = transaction.hash; let memo = txMetadata[transaction.hash]?.memo?.trim() ?? ''; let status; if (wallet.chain === Chain.OFFCHAIN) { hash = transaction.payment_hash; memo = transaction.description; status = transaction.ispaid ? loc._.success : loc.lnd.expired; if (hash?.type === 'Buffer' && hash?.data) { hash = Buffer.from(hash.data).toString('hex'); } } const data = [new Date(transaction.received).toString(), 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() > 0 && 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]); 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(); } catch (error) { return error.message; } })()} ); } })()} {loc.wallets.add_wallet_name.toLowerCase()} {loc.wallets.details_type.toLowerCase()} {wallet.typeReadable} {wallet.type === LightningLdkWallet.type && ( <> {loc.wallets.identity_pubkey} {lightningWalletInfo?.identityPubkey ? ( <> {lightningWalletInfo.identityPubkey} ) : ( )} )} {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()} setBackdoorBip47Pressed(prevState => prevState + 1)}>{loc.wallets.details_display} <> {loc.transactions.transactions_count.toLowerCase()} {wallet.getTransactions().length} {backdoorBip47Pressed >= 10 && wallet.allowBIP47() ? ( <> {loc.bip47.payment_code} {loc.bip47.purpose} ) : null} {wallet.type === WatchOnlyWallet.type && wallet.isHd() && ( <> {loc.wallets.details_advanced.toLowerCase()} {loc.wallets.details_use_with_hardware_wallet} )} {isAdvancedModeEnabled && ( {wallet.allowMasterFingerprint() && ( {loc.wallets.details_master_fingerprint.toLowerCase()} {masterFingerprint ?? } )} {derivationPath && ( {loc.wallets.details_derivation_path} {derivationPath} )} )} {(wallet instanceof AbstractHDElectrumWallet || (wallet.type === WatchOnlyWallet.type && wallet.isHd())) && ( )} {wallet.allowBIP47() && isBIP47Enabled && }