From a76c847a108724c3d4d883a60e77693cfec94bf6 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Sun, 29 Dec 2024 01:26:10 -0400 Subject: [PATCH 1/9] ADD: Set preferred server from menu --- blue_modules/BlueElectrum.ts | 166 ++++++++++----- components/TooltipMenu.tsx | 8 + loc/en.json | 9 +- screen/settings/ElectrumSettings.tsx | 302 +++++++++++++++++++-------- typings/CommonToolTipActions.ts | 4 +- 5 files changed, 335 insertions(+), 154 deletions(-) diff --git a/blue_modules/BlueElectrum.ts b/blue_modules/BlueElectrum.ts index 8e1cfc83d..6a004ce00 100644 --- a/blue_modules/BlueElectrum.ts +++ b/blue_modules/BlueElectrum.ts @@ -1,4 +1,3 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; import BigNumber from 'bignumber.js'; import * as bitcoin from 'bitcoinjs-lib'; import DefaultPreference from 'react-native-default-preference'; @@ -9,6 +8,7 @@ import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet, TaprootWallet } fro import presentAlert from '../components/Alert'; import loc from '../loc'; import { GROUP_IO_BLUEWALLET } from './currency'; +import { ElectrumServerItem } from '../screen/settings/ElectrumSettings'; const ElectrumClient = require('electrum-client'); const net = require('net'); @@ -70,12 +70,12 @@ type MempoolTransaction = { type Peer = | { host: string; - ssl: string; + ssl: number; tcp?: undefined; } | { host: string; - tcp: string; + tcp: number; ssl?: undefined; }; @@ -85,16 +85,20 @@ export const ELECTRUM_SSL_PORT = 'electrum_ssl_port'; export const ELECTRUM_SERVER_HISTORY = 'electrum_server_history'; const ELECTRUM_CONNECTION_DISABLED = 'electrum_disabled'; const storageKey = 'ELECTRUM_PEERS'; -const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: '443' }; +const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: 443 }; export const hardcodedPeers: Peer[] = [ - { host: 'mainnet.foundationdevices.com', ssl: '50002' }, - // { host: 'bitcoin.lukechilds.co', ssl: '50002' }, + { host: 'mainnet.foundationdevices.com', ssl: 50002 }, + // { host: 'bitcoin.lukechilds.co', ssl: 50002 }, // { host: 'electrum.jochen-hoenicke.de', ssl: '50006' }, - { host: 'electrum1.bluewallet.io', ssl: '443' }, - { host: 'electrum.acinq.co', ssl: '50002' }, - { host: 'electrum.bitaroo.net', ssl: '50002' }, + { host: 'electrum1.bluewallet.io', ssl: 443 }, + { host: 'electrum.acinq.co', ssl: 50002 }, + { host: 'electrum.bitaroo.net', ssl: 50002 }, ]; +export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({ + ...peer, +})); + let mainClient: typeof ElectrumClient | undefined; let mainConnected: boolean = false; let wasConnectedAtLeastOnce: boolean = false; @@ -137,23 +141,56 @@ async function _getRealm() { return _realm; } +export const getPreferredServer = async (): Promise => { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string; + const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT); + const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT); + + console.log('Getting preferred server:', { host, tcpPort, sslPort }); + + if (!host) { + console.warn('Preferred server host is undefined'); + return; + } + + return { + host, + tcp: tcpPort ? Number(tcpPort) : undefined, + ssl: sslPort ? Number(sslPort) : undefined, + }; +}; + +export const removePreferredServer = async () => { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + console.log('Removing preferred server'); + await DefaultPreference.clear(ELECTRUM_HOST); + await DefaultPreference.clear(ELECTRUM_TCP_PORT); + await DefaultPreference.clear(ELECTRUM_SSL_PORT); +}; + export async function isDisabled(): Promise { let result; try { - const savedValue = await AsyncStorage.getItem(ELECTRUM_CONNECTION_DISABLED); + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + const savedValue = await DefaultPreference.get(ELECTRUM_CONNECTION_DISABLED); + console.log('Getting Electrum connection disabled state:', savedValue); if (savedValue === null) { result = false; } else { result = savedValue; } - } catch { + } catch (error) { + console.error('Error getting Electrum connection disabled state:', error); result = false; } return !!result; } export async function setDisabled(disabled = true) { - return AsyncStorage.setItem(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : ''); + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + console.log('Setting Electrum connection disabled state to:', disabled); + return DefaultPreference.set(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : ''); } function getCurrentPeer() { @@ -171,20 +208,23 @@ function getNextPeer() { } async function getSavedPeer(): Promise { - const host = await AsyncStorage.getItem(ELECTRUM_HOST); - const tcpPort = await AsyncStorage.getItem(ELECTRUM_TCP_PORT); - const sslPort = await AsyncStorage.getItem(ELECTRUM_SSL_PORT); + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string; + const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT); + const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT); + + console.log('Getting saved peer:', { host, tcpPort, sslPort }); if (!host) { return null; } if (sslPort) { - return { host, ssl: sslPort }; + return { host, ssl: Number(sslPort) }; } if (tcpPort) { - return { host, tcp: tcpPort }; + return { host, tcp: Number(tcpPort) }; } return null; @@ -201,6 +241,8 @@ export async function connectMain(): Promise { usingPeer = savedPeer; } + console.log('Using peer:', JSON.stringify(usingPeer)); + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); try { if (usingPeer.host.endsWith('onion')) { @@ -208,10 +250,6 @@ export async function connectMain(): Promise { await DefaultPreference.set(ELECTRUM_HOST, randomPeer.host); await DefaultPreference.set(ELECTRUM_TCP_PORT, randomPeer.tcp ?? ''); await DefaultPreference.set(ELECTRUM_SSL_PORT, randomPeer.ssl ?? ''); - } else { - await DefaultPreference.set(ELECTRUM_HOST, usingPeer.host); - await DefaultPreference.set(ELECTRUM_TCP_PORT, usingPeer.tcp ?? ''); - await DefaultPreference.set(ELECTRUM_SSL_PORT, usingPeer.ssl ?? ''); } } catch (e) { // Must be running on Android @@ -292,6 +330,38 @@ export async function connectMain(): Promise { } } +export async function presentResetToDefaultsAlert(): Promise { + return new Promise(resolve => { + presentAlert({ + title: loc.settings.electrum_reset, + message: loc.settings.electrum_reset_to_default, + buttons: [ + { + text: loc._.cancel, + style: 'cancel', + onPress: () => resolve(false), + }, + { + text: loc._.ok, + style: 'destructive', + onPress: async () => { + try { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + await DefaultPreference.clear(ELECTRUM_HOST); + await DefaultPreference.clear(ELECTRUM_SSL_PORT); + await DefaultPreference.clear(ELECTRUM_TCP_PORT); + } catch (e) { + console.log(e); // Must be running on Android + } + resolve(true); + }, + }, + ], + options: { cancelable: true }, + }); + }); +} + const presentNetworkErrorAlert = async (usingPeer?: Peer) => { if (await isDisabled()) { console.log( @@ -299,6 +369,7 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => { ); return; } + presentAlert({ allowRepeat: false, title: loc.errors.network, @@ -319,39 +390,13 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => { { text: loc.settings.electrum_reset, onPress: () => { - presentAlert({ - title: loc.settings.electrum_reset, - message: loc.settings.electrum_reset_to_default, - buttons: [ - { - text: loc._.cancel, - style: 'cancel', - onPress: () => {}, - }, - { - text: loc._.ok, - style: 'destructive', - onPress: async () => { - await AsyncStorage.setItem(ELECTRUM_HOST, ''); - await AsyncStorage.setItem(ELECTRUM_TCP_PORT, ''); - await AsyncStorage.setItem(ELECTRUM_SSL_PORT, ''); - try { - await DefaultPreference.setName('group.io.bluewallet.bluewallet'); - await DefaultPreference.clear(ELECTRUM_HOST); - await DefaultPreference.clear(ELECTRUM_SSL_PORT); - await DefaultPreference.clear(ELECTRUM_TCP_PORT); - } catch (e) { - console.log(e); // Must be running on Android - } - presentAlert({ message: loc.settings.electrum_saved }); - setTimeout(connectMain, 500); - }, - }, - ], - options: { cancelable: true }, + presentResetToDefaultsAlert().then(result => { + if (result) { + connectionAttempt = 0; + mainClient.close() && mainClient.close(); + setTimeout(connectMain, 500); + } }); - connectionAttempt = 0; - mainClient.close() && mainClient.close(); }, style: 'destructive', }, @@ -377,13 +422,18 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars async function getRandomDynamicPeer(): Promise { try { - let peers = JSON.parse((await AsyncStorage.getItem(storageKey)) as string); + let peers = JSON.parse((await DefaultPreference.get(storageKey)) as string); peers = peers.sort(() => Math.random() - 0.5); // shuffle for (const peer of peers) { - const ret = { - host: peer[1] as string, - tcp: '', - }; + const ret: Peer = { host: peer[0], ssl: peer[1] }; + ret.host = peer[1]; + + if (peer[1] === 's') { + ret.ssl = peer[2]; + } else { + ret.tcp = peer[2]; + } + for (const item of peer[2]) { if (item.startsWith('t')) { ret.tcp = item.replace('t', ''); diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index 39a5be982..820d4027f 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -55,6 +55,14 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref) => { image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined, state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState), attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden }, + subactions: subaction.subactions?.map(subsubaction => ({ + id: subsubaction.id.toString(), + title: subsubaction.text, + subtitle: subsubaction.subtitle, + image: subsubaction.icon?.iconValue ? subsubaction.icon.iconValue : undefined, + state: subsubaction.menuState === undefined ? undefined : ((subsubaction.menuState ? 'on' : 'off') as MenuState), + attributes: { disabled: subsubaction.disabled, destructive: subsubaction.destructive, hidden: subsubaction.hidden }, + })), })) || []; return { diff --git a/loc/en.json b/loc/en.json index c828b5e4b..3b6167935 100644 --- a/loc/en.json +++ b/loc/en.json @@ -28,6 +28,7 @@ "enter_amount": "Enter amount", "qr_custom_input_button": "Tap 10 times to enter custom input", "unlock": "Unlock", + "port": "Port", "suggested": "Suggested" }, "azteco": { @@ -253,11 +254,11 @@ "electrum_preferred_server_description": "Enter the server you want your wallet to use for all Bitcoin activities. Once set, your wallet will exclusively use this server to check balances, send transactions, and fetch network data. Ensure you trust this server before setting it.", "electrum_clear_alert_title": "Clear history?", "electrum_clear_alert_message": "Do you want to clear electrum servers history?", "electrum_clear_alert_cancel": "Cancel", - "electrum_clear_alert_ok": "Ok", - "electrum_reset": "Reset to default", + "only_use_preferred": "Only connect to preferred server", "electrum_unable_to_connect": "Unable to connect to {server}.", "electrum_history": "History", - "electrum_reset_to_default": "Are you sure to want to reset your Electrum settings to default?", + "electrum_reset_to_default": "This will let BlueWallet randomly choose a server from the suggested list and history. Your server history will remain unchanged.", + "electrum_reset": "Reset to default", "electrum_clear": "Clear History", "encrypt_decrypt": "Decrypt Storage", "encrypt_decrypt_q": "Are you sure you want to decrypt your storage? This will allow your wallets to be accessed without a password.", @@ -272,6 +273,7 @@ "encrypt_title": "Security", "encrypt_tstorage": "Storage", "encrypt_use": "Use {type}", + "set_as_preferred": "Set as preferred", "encrypted_feature_disabled": "This feature cannot be used with encrypted storage enabled.", "encrypt_use_expl": "{type} will be used to confirm your identity before making a transaction, unlocking, exporting, or deleting a wallet. {type} will not be used to unlock encrypted storage.", "biometrics_fail": "If {type} is not enabled, or fails to unlock, you can use your device passcode as an alternative.", @@ -291,6 +293,7 @@ "network": "Network", "network_broadcast": "Broadcast Transaction", "network_electrum": "Electrum Server", + "electrum_suggested_description": "When a preferred server is not set, a suggested server will be selected for use at random.", "not_a_valid_uri": "Invalid URI", "notifications": "Notifications", "open_link_in_explorer": "Open link in explorer", diff --git a/screen/settings/ElectrumSettings.tsx b/screen/settings/ElectrumSettings.tsx index cd0384c22..a5f5469a0 100644 --- a/screen/settings/ElectrumSettings.tsx +++ b/screen/settings/ElectrumSettings.tsx @@ -22,21 +22,25 @@ import { CommonToolTipActions } from '../../typings/CommonToolTipActions'; import { Divider } from '@rneui/themed'; import { Header } from '../../components/Header'; import AddressInput from '../../components/AddressInput'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { GROUP_IO_BLUEWALLET } from '../../blue_modules/currency'; import { Action } from '../../components/types'; import ListItem, { PressableWrapper } from '../../components/ListItem'; import HeaderMenuButton from '../../components/HeaderMenuButton'; import { useSettings } from '../../hooks/context/useSettings'; +import { suggestedServers, hardcodedPeers, presentResetToDefaultsAlert } from '../../blue_modules/BlueElectrum'; type RouteProps = RouteProp; export interface ElectrumServerItem { host: string; - port?: number; - sslPort?: number; + tcp?: number; + ssl?: number; } +const SET_PREFERRED_PREFIX = 'set_preferred_'; +const DELETE_PREFIX = 'delete_'; +const PREFERRED_SERVER_ROW = 'preferredserverrow'; + const ElectrumSettings: React.FC = () => { const { colors } = useTheme(); const { server } = useRoute().params; @@ -79,36 +83,54 @@ const ElectrumSettings: React.FC = () => { }, }); - useEffect(() => { - let configInterval: NodeJS.Timeout | null = null; - const fetchData = async () => { - const savedHost = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_HOST); - const savedPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_TCP_PORT); - const savedSslPort = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SSL_PORT); - const serverHistoryStr = await AsyncStorage.getItem(BlueElectrum.ELECTRUM_SERVER_HISTORY); + const configIntervalRef = React.useRef(null); - const parsedServerHistory: ElectrumServerItem[] = serverHistoryStr ? JSON.parse(serverHistoryStr) : []; + const fetchData = React.useCallback(async () => { + console.log('Fetching data...'); + const preferredServer = await BlueElectrum.getPreferredServer(); + const savedHost = preferredServer?.host; + const savedPort = preferredServer?.tcp; + const savedSslPort = preferredServer?.ssl; + const serverHistoryStr = (await DefaultPreference.get(BlueElectrum.ELECTRUM_SERVER_HISTORY)) as string; - setHost(savedHost || ''); - setPort(savedPort ? Number(savedPort) : undefined); - setSslPort(savedSslPort ? Number(savedSslPort) : undefined); - setServerHistory(parsedServerHistory); + console.log('Preferred server:', preferredServer); + console.log('Server history string:', serverHistoryStr); + const parsedServerHistory: ElectrumServerItem[] = serverHistoryStr ? JSON.parse(serverHistoryStr) : []; + const filteredServerHistory = parsedServerHistory.filter( + v => + v.host && + (v.tcp || v.ssl) && + !suggestedServers.some(suggested => suggested.host === v.host && suggested.tcp === v.tcp && suggested.ssl === v.ssl) && + !hardcodedPeers.some(peer => peer.host === v.host), + ); + + console.log('Filtered server history:', filteredServerHistory); + + setHost(savedHost || ''); + setPort(savedPort ? Number(savedPort) : undefined); + setSslPort(savedSslPort ? Number(savedSslPort) : undefined); + setServerHistory(filteredServerHistory); + + setConfig(await BlueElectrum.getConfig()); + configIntervalRef.current = setInterval(async () => { setConfig(await BlueElectrum.getConfig()); - configInterval = setInterval(async () => { - setConfig(await BlueElectrum.getConfig()); - }, 500); + }, 500); - setIsLoading(false); - }; - - fetchData(); + setIsLoading(false); return () => { - if (configInterval) clearInterval(configInterval); + if (configIntervalRef.current) clearInterval(configIntervalRef.current); }; }, []); + useEffect(() => { + fetchData(); + return () => { + if (configIntervalRef.current) clearInterval(configIntervalRef.current); + }; + }, [fetchData]); + useEffect(() => { if (server) { triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy); @@ -132,56 +154,60 @@ const ElectrumSettings: React.FC = () => { const clearHistory = useCallback(async () => { setIsLoading(true); - await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify([])); + await DefaultPreference.clear(BlueElectrum.ELECTRUM_SERVER_HISTORY); setServerHistory([]); setIsLoading(false); }, []); const serverExists = useCallback( (value: ElectrumServerItem) => { - return serverHistory.some(s => `${s.host}:${s.port}:${s.sslPort}` === `${value.host}:${value.port}:${value.sslPort}`); + return serverHistory.some(s => `${s.host}:${s.tcp}:${s.ssl}` === `${value.host}:${value.tcp}:${value.ssl}`); }, [serverHistory], ); - const save = useCallback(async () => { - Keyboard.dismiss(); - setIsLoading(true); + const save = useCallback( + async (v?: ElectrumServerItem) => { + Keyboard.dismiss(); + setIsLoading(true); - try { - if (!host && !port && !sslPort) { - await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_HOST); - await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_TCP_PORT); - await AsyncStorage.removeItem(BlueElectrum.ELECTRUM_SSL_PORT); - await DefaultPreference.setName(GROUP_IO_BLUEWALLET); - await DefaultPreference.clear(BlueElectrum.ELECTRUM_HOST); - await DefaultPreference.clear(BlueElectrum.ELECTRUM_TCP_PORT); - await DefaultPreference.clear(BlueElectrum.ELECTRUM_SSL_PORT); - } else { - await AsyncStorage.setItem(BlueElectrum.ELECTRUM_HOST, host); - await AsyncStorage.setItem(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || ''); - await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || ''); - if (!serverExists({ host, port, sslPort })) { - const newServerHistory = [...serverHistory, { host, port, sslPort }]; - await AsyncStorage.setItem(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(newServerHistory)); - setServerHistory(newServerHistory); + try { + const serverHost = v?.host || host; + const serverPort = v?.tcp || port?.toString() || ''; + const serverSslPort = v?.ssl || sslPort?.toString() || ''; + + if (serverHost && (serverPort || serverSslPort)) { + await DefaultPreference.setName(GROUP_IO_BLUEWALLET); + await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, serverHost); + await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, serverPort); + await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, serverSslPort); + + if ( + !serverExists({ host: serverHost, tcp: Number(serverPort), ssl: Number(serverSslPort) }) && + serverHost && + (serverPort || serverSslPort) && + !hardcodedPeers.some(peer => peer.host === serverHost) + ) { + const newServerHistory = [...serverHistory, { host: serverHost, tcp: Number(serverPort), ssl: Number(serverSslPort) }]; + await DefaultPreference.set(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(newServerHistory)); + setServerHistory(newServerHistory); + } } - await DefaultPreference.setName(GROUP_IO_BLUEWALLET); - await DefaultPreference.set(BlueElectrum.ELECTRUM_HOST, host); - await DefaultPreference.set(BlueElectrum.ELECTRUM_TCP_PORT, port?.toString() || ''); - await DefaultPreference.set(BlueElectrum.ELECTRUM_SSL_PORT, sslPort?.toString() || ''); + + triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); + presentAlert({ message: loc.settings.electrum_saved }); + } catch (error) { + triggerHapticFeedback(HapticFeedbackTypes.NotificationError); + presentAlert({ message: (error as Error).message }); + } finally { + setIsLoading(false); } - triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess); - presentAlert({ message: loc.settings.electrum_saved }); - } catch (error) { - triggerHapticFeedback(HapticFeedbackTypes.NotificationError); - presentAlert({ message: (error as Error).message }); - } - setIsLoading(false); - }, [host, port, sslPort, serverExists, serverHistory]); + }, + [host, port, sslPort, serverExists, serverHistory], + ); const resetToDefault = useCallback(() => { - Alert.alert(loc.settings.electrum_reset, loc.settings.electrum_reset_to_default, [ + Alert.alert(loc.settings.electrum_preferred_server, loc.settings.electrum_preferred_server_description, [ { text: loc._.cancel, onPress: () => console.log('Cancel Pressed'), @@ -191,22 +217,22 @@ const ElectrumSettings: React.FC = () => { text: loc._.ok, style: 'destructive', onPress: async () => { + await BlueElectrum.removePreferredServer(); setHost(''); setPort(undefined); setSslPort(undefined); - await save(); }, }, ]); - }, [save]); + }, []); const selectServer = useCallback( (value: string) => { const parsedServer = JSON.parse(value) as ElectrumServerItem; setHost(parsedServer.host); - setPort(parsedServer.port); - setSslPort(parsedServer.sslPort); - save(); + setPort(parsedServer.tcp); + setSslPort(parsedServer.ssl); + save(parsedServer); }, [save], ); @@ -215,55 +241,139 @@ const ElectrumSettings: React.FC = () => { triggerHapticFeedback(HapticFeedbackTypes.ImpactHeavy); Alert.alert(loc.settings.electrum_clear_alert_title, loc.settings.electrum_clear_alert_message, [ { text: loc.settings.electrum_clear_alert_cancel, onPress: () => console.log('Cancel Pressed'), style: 'cancel' }, - { text: loc.settings.electrum_clear_alert_ok, onPress: () => clearHistory() }, + { text: loc._.ok, onPress: () => clearHistory() }, ]); }, [clearHistory]); const onPressMenuItem = useCallback( (id: string) => { - switch (id) { - case CommonToolTipActions.ResetToDefault.id: - resetToDefault(); - break; - case CommonToolTipActions.ClearHistory.id: - clearHistoryAlert(); - break; - default: - try { - selectServer(id); - } catch (error) { - console.warn('Unknown menu item selected:', id); + if (id.startsWith(SET_PREFERRED_PREFIX)) { + const rawServer = JSON.parse(id.replace(SET_PREFERRED_PREFIX, '')); + selectServer(JSON.stringify(rawServer)); + } else if (id.startsWith(DELETE_PREFIX)) { + const rawServer = JSON.parse(id.replace(DELETE_PREFIX, '')); + const newServerHistory = serverHistory + .filter(s => !(s.host === rawServer.host && s.tcp === rawServer.tcp && s.ssl === rawServer.ssl)) + .filter( + v => !suggestedServers.some(suggested => suggested.host === v.host && suggested.tcp === v.tcp && suggested.ssl === v.ssl), + ); + setServerHistory(newServerHistory); + DefaultPreference.set(BlueElectrum.ELECTRUM_SERVER_HISTORY, JSON.stringify(newServerHistory)); + } else if (id === PREFERRED_SERVER_ROW) { + presentResetToDefaultsAlert().then(async result => { + if (result) { + await BlueElectrum.removePreferredServer(); + fetchData(); } - break; + }); + } else { + switch (id) { + case CommonToolTipActions.ResetToDefault.id: + resetToDefault(); + break; + case CommonToolTipActions.ClearHistory.id: + clearHistoryAlert(); + break; + default: + try { + selectServer(id); + } catch (error) { + console.warn('Unknown menu item selected:', id); + } + break; + } } }, - [clearHistoryAlert, resetToDefault, selectServer], + [selectServer, serverHistory, fetchData, resetToDefault, clearHistoryAlert], ); - const toolTipActions = useMemo(() => { - const actions: Action[] = [CommonToolTipActions.ResetToDefault]; + const createServerAction = useCallback( + (value: ElectrumServerItem, seenHosts: Set) => { + const hostKey = `${value.host}:${value.ssl ?? value.tcp}`; + if (seenHosts.has(hostKey)) return null; - if (serverHistory.length > 0) { - const serverSubactions: Action[] = serverHistory.map(value => ({ + seenHosts.add(hostKey); + return { id: JSON.stringify(value), - text: `${value.host}`, - subtitle: `${value.port || value.sslPort}`, - menuState: `${host}:${port}:${sslPort}` === `${value.host}:${value.port}:${value.sslPort}`, - disabled: isLoading || (host === value.host && (port === value.port || sslPort === value.sslPort)), - })); + text: Platform.OS === 'android' ? `${value.host}:${value.ssl ?? value.tcp}` : value.host, + subactions: [ + ...(host === value.host && (port === value.tcp || sslPort === value.ssl) + ? [] + : [ + { + id: `${SET_PREFERRED_PREFIX}${JSON.stringify(value)}`, + text: loc.settings.set_as_preferred, + subtitle: `${loc._.port}: ${value.ssl ?? value.tcp}`, + }, + ]), + ...(hardcodedPeers.some(peer => peer.host === value.host) + ? [] + : [ + { + id: `${DELETE_PREFIX}${JSON.stringify(value)}`, + text: loc.wallets.details_delete, + }, + ]), + ], + } as Action; + }, + [host, port, sslPort], + ); + + const generateToolTipActions = useCallback(() => { + const actions: Action[] = []; + const seenHosts = new Set(); + + if (host) { + const preferred = { + id: 'preferred', + hidden: false, + displayInline: true, + text: loc.settings.electrum_preferred_server, + subactions: [ + { + id: PREFERRED_SERVER_ROW, + text: Platform.OS === 'android' ? `${host} (${sslPort ?? port})` : host, + subtitle: `${loc._.port}: ${sslPort ?? port}`, + menuState: true, + }, + ], + }; + actions.push(preferred); + seenHosts.add(`${host}:${sslPort ?? port}`); + } + + const suggestedServersAction: Action = { + id: 'suggested_servers', + text: loc._.suggested, + displayInline: true, + subtitle: loc.settings.electrum_suggested_description, + subactions: suggestedServers.map(value => createServerAction(value, seenHosts)).filter((action): action is Action => action !== null), + }; + + actions.push(suggestedServersAction); + + console.warn('serverHistory', serverHistory); + if (serverHistory.length > 0) { + const serverSubactions: Action[] = serverHistory + .map(value => createServerAction(value, seenHosts)) + .filter((action): action is Action => action !== null); actions.push({ id: 'server_history', text: loc.settings.electrum_history, + displayInline: serverHistory.length <= 5 && serverHistory.length > 0, subactions: [CommonToolTipActions.ClearHistory, ...serverSubactions], + hidden: serverHistory.length === 0, }); } + return actions; - }, [host, isLoading, port, serverHistory, sslPort]); + }, [createServerAction, host, port, serverHistory, sslPort]); const HeaderRight = useMemo( - () => , - [onPressMenuItem, toolTipActions], + () => , + [onPressMenuItem, generateToolTipActions], ); useEffect(() => { @@ -331,6 +441,8 @@ const ElectrumSettings: React.FC = () => { } }; + const preferredServerIsEmpty = !host || (!port && !sslPort); + const renderElectrumSettings = () => { return ( <> @@ -411,13 +523,19 @@ const ElectrumSettings: React.FC = () => { testID="SSLPortInput" value={sslPort !== undefined} onValueChange={onSSLPortChange} - disabled={host?.endsWith('.onion') ?? false} + disabled={host?.endsWith('.onion') || isLoading || host === '' || (port === undefined && sslPort === undefined)} /> -