diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e919961dbf..97424431f97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# edge-react-gui
+## Unreleased (develop)
+
+- added: Debug settings scene (Developer Mode only) with nodes/servers inspection, engine `dataDump` viewer, and log viewer
+
## 4.45.0 (staging)
- fixed: Fixed Zano token minting transaction detection issues.
diff --git a/src/components/Main.tsx b/src/components/Main.tsx
index ac7c71d8729..71de1794e5d 100644
--- a/src/components/Main.tsx
+++ b/src/components/Main.tsx
@@ -71,6 +71,7 @@ import { CreateWalletImportScene as CreateWalletImportSceneComponent } from './s
import { CreateWalletSelectCryptoScene as CreateWalletSelectCryptoSceneComponent } from './scenes/CreateWalletSelectCryptoScene'
import { CurrencyNotificationScene as CurrencyNotificationSceneComponent } from './scenes/CurrencyNotificationScene'
import { CurrencySettingsScene as CurrencySettingsSceneComponent } from './scenes/CurrencySettingsScene'
+import { DebugScene as DebugSceneComponent } from './scenes/DebugScene'
import { DefaultFiatSettingScene as DefaultFiatSettingSceneComponent } from './scenes/DefaultFiatSettingScene'
import { DevTestScene } from './scenes/DevTestScene'
import { DuressModeHowToScene as DuressModeHowToSceneComponent } from './scenes/DuressModeHowToScene'
@@ -209,6 +210,7 @@ const CreateWalletSelectFiatScene = ifLoggedIn(
)
const CurrencyNotificationScene = ifLoggedIn(CurrencyNotificationSceneComponent)
const CurrencySettingsScene = ifLoggedIn(CurrencySettingsSceneComponent)
+const DebugScene = ifLoggedIn(DebugSceneComponent)
const DefaultFiatSettingScene = ifLoggedIn(DefaultFiatSettingSceneComponent)
const EarnScene = ifLoggedIn(EarnSceneComponent)
const EdgeLoginScene = ifLoggedIn(EdgeLoginSceneComponent)
@@ -1105,6 +1107,13 @@ const EdgeAppStack: React.FC = () => {
options={{ headerShown: false }}
/>
+
+
+interface DumpResult {
+ dump?: EdgeDataDump
+ error?: string
+}
+
+interface NodesWalletSectionProps {
+ wallet: EdgeCurrencyWallet
+ dumpResult: DumpResult | undefined
+ isExpanded: boolean
+ isLoading: boolean
+ onToggle: (walletId: string) => void
+ onLongPress: (walletId: string) => void
+}
+
+interface DumpWalletRowProps {
+ wallet: EdgeCurrencyWallet
+ dumpResult: DumpResult | undefined
+ isExpanded: boolean
+ isLoading: boolean
+ onPress: (walletId: string) => void
+ onLongPress: (walletId: string) => void
+}
+
+// ---------------------------------------------------------------------------
+// Main scene
+// ---------------------------------------------------------------------------
+
+export const DebugScene: React.FC = () => {
+ const theme = useTheme()
+ const styles = getStyles(theme)
+ const account = useSelector(state => state.core.account)
+ const currencyWallets = useWatch(account, 'currencyWallets')
+ const wallets = React.useMemo(
+ () => Object.values(currencyWallets),
+ [currencyWallets]
+ )
+
+ const [showNodesAndServers, setShowNodesAndServers] = React.useState(false)
+ const [showDataDump, setShowDataDump] = React.useState(false)
+ const [showLogs, setShowLogs] = React.useState(false)
+ const [walletDumpMap, setWalletDumpMap] = React.useState<
+ Record
+ >({})
+ const [walletExpandedMap, setWalletExpandedMap] = React.useState<
+ Record
+ >({})
+ const [loadingWallets, setLoadingWallets] = React.useState<
+ Record
+ >({})
+ const [logsInfo, setLogsInfo] = React.useState('')
+ const [logsActivity, setLogsActivity] = React.useState('')
+ const [showInfoLog, setShowInfoLog] = React.useState(false)
+ const [showActivityLog, setShowActivityLog] = React.useState(false)
+
+ const logsLoadedRef = React.useRef(false)
+
+ // --- Core async operations ---
+
+ const loadWalletDump = useHandler((walletId: string): void => {
+ const wallet = currencyWallets[walletId]
+ if (wallet == null) return
+ setLoadingWallets(prev => ({ ...prev, [walletId]: true }))
+ wallet
+ .dumpData()
+ .then(dump => {
+ setWalletDumpMap(prev => ({ ...prev, [walletId]: { dump } }))
+ })
+ .catch((error: unknown) => {
+ setWalletDumpMap(prev => ({
+ ...prev,
+ [walletId]: {
+ error: error instanceof Error ? error.message : String(error)
+ }
+ }))
+ showError(error)
+ })
+ .finally(() => {
+ setLoadingWallets(prev => ({ ...prev, [walletId]: false }))
+ })
+ })
+
+ const handleRefreshLogs = useHandler(async (): Promise => {
+ const [info, activity] = await Promise.all([
+ readLogs('info'),
+ readLogs('activity')
+ ])
+ setLogsInfo(info ?? '')
+ setLogsActivity(activity ?? '')
+ })
+
+ // --- Section toggle handlers ---
+
+ const handleToggleNodesAndServers = useHandler(() => {
+ setShowNodesAndServers(prev => !prev)
+ })
+
+ const handleToggleDataDump = useHandler(() => {
+ setShowDataDump(prev => !prev)
+ })
+
+ const handleToggleLogs = useHandler(() => {
+ setShowLogs(prev => !prev)
+ })
+
+ // --- Long press (copy) handlers for top-level sections ---
+
+ const handleCopyJson = useHandler((json: unknown, label: string): void => {
+ try {
+ Clipboard.setString(JSON.stringify(json, null, 2))
+ showToast(sprintf(lstrings.settings_debug_copied_1s, label))
+ } catch (error: unknown) {
+ showError(error)
+ }
+ })
+
+ const handleLongPressNodesAndServers = useHandler(() => {
+ const nodesData: Record = {}
+ for (const wallet of wallets) {
+ const data = walletDumpMap[wallet.id]?.dump?.data
+ if (data != null) {
+ nodesData[wallet.id] = {
+ label: getWalletLabel(wallet),
+ defaultServers: data.defaultServers ?? {},
+ infoServerServers: data.infoServerServers ?? {},
+ customServers: data.customServers ?? {},
+ userSettings: wallet.currencyConfig.userSettings ?? {},
+ ...(data.networkConfig != null
+ ? { networkConfig: data.networkConfig }
+ : {})
+ }
+ }
+ }
+ handleCopyJson(nodesData, lstrings.settings_debug_nodes_servers)
+ })
+
+ const handleLongPressDataDump = useHandler(() => {
+ const dumpData: Record = {}
+ for (const wallet of wallets) {
+ const dump = walletDumpMap[wallet.id]?.dump
+ if (dump != null) {
+ dumpData[wallet.id] = {
+ label: getWalletLabel(wallet),
+ dump
+ }
+ }
+ }
+ handleCopyJson(dumpData, lstrings.settings_debug_engine_dump)
+ })
+
+ const handleLongPressLogs = useHandler(() => {
+ const allLogs = `=== Info Log ===\n${logsInfo}\n\n=== Activity Log ===\n${logsActivity}`
+ Clipboard.setString(allLogs)
+ showToast(
+ sprintf(lstrings.settings_debug_copied_1s, lstrings.settings_debug_logs)
+ )
+ })
+
+ // --- Long press (copy) handlers for log sub-sections ---
+
+ const handleLongPressInfoLog = useHandler(() => {
+ Clipboard.setString(logsInfo)
+ showToast(
+ sprintf(
+ lstrings.settings_debug_copied_1s,
+ lstrings.settings_debug_info_log
+ )
+ )
+ })
+
+ const handleLongPressActivityLog = useHandler(() => {
+ Clipboard.setString(logsActivity)
+ showToast(
+ sprintf(
+ lstrings.settings_debug_copied_1s,
+ lstrings.settings_debug_activity_log
+ )
+ )
+ })
+
+ // --- Per-wallet handlers ---
+
+ const handleNodesWalletToggle = useHandler((walletId: string): void => {
+ setWalletExpandedMap(prev => ({
+ ...prev,
+ [`nodes:${walletId}`]: !(prev[`nodes:${walletId}`] ?? false)
+ }))
+ })
+
+ const handleNodesWalletLongPress = useHandler((walletId: string): void => {
+ const wallet = account.currencyWallets[walletId]
+ const data = walletDumpMap[walletId]?.dump?.data
+ if (data != null && wallet != null) {
+ const content = {
+ defaultServers: data.defaultServers ?? {},
+ infoServerServers: data.infoServerServers ?? {},
+ customServers: data.customServers ?? {},
+ userSettings: wallet.currencyConfig.userSettings ?? {},
+ ...(data.networkConfig != null
+ ? { networkConfig: data.networkConfig }
+ : {})
+ }
+ const label = getWalletLabel(wallet)
+ handleCopyJson(content, label)
+ }
+ })
+
+ const handleDumpWalletPress = useHandler((walletId: string): void => {
+ // `walletDumpMap` is shared with Nodes & Servers; that section may have
+ // already loaded the dump for this wallet.
+ const dumpResult = walletDumpMap[walletId]
+ if (dumpResult?.dump == null && !(loadingWallets[walletId] ?? false)) {
+ loadWalletDump(walletId)
+ setWalletExpandedMap(prev => ({
+ ...prev,
+ [`dump:${walletId}`]: true
+ }))
+ } else {
+ setWalletExpandedMap(prev => ({
+ ...prev,
+ [`dump:${walletId}`]: !(prev[`dump:${walletId}`] ?? false)
+ }))
+ }
+ })
+
+ const handleDumpWalletLongPress = useHandler((walletId: string): void => {
+ const dump = walletDumpMap[walletId]?.dump
+ if (dump != null) {
+ const wallet = account.currencyWallets[walletId]
+ const label = wallet != null ? getWalletLabel(wallet) : walletId
+ handleCopyJson(dump, label)
+ }
+ })
+
+ const handleToggleInfoLog = useHandler(() => {
+ setShowInfoLog(prev => !prev)
+ })
+
+ const handleToggleActivityLog = useHandler(() => {
+ setShowActivityLog(prev => !prev)
+ })
+
+ // --- Auto-load effects ---
+
+ React.useEffect(() => {
+ if (!showNodesAndServers) return
+ for (const wallet of wallets) {
+ if (
+ walletDumpMap[wallet.id] == null &&
+ !(loadingWallets[wallet.id] ?? false)
+ ) {
+ loadWalletDump(wallet.id)
+ }
+ }
+ }, [
+ loadingWallets,
+ loadWalletDump,
+ showNodesAndServers,
+ walletDumpMap,
+ wallets
+ ])
+
+ React.useEffect(() => {
+ if (showLogs && !logsLoadedRef.current) {
+ logsLoadedRef.current = true
+ handleRefreshLogs().catch((error: unknown) => {
+ showError(error)
+ })
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [showLogs])
+
+ // --- Render ---
+
+ return (
+
+
+ {lstrings.settings_debug_long_press_hint}
+
+
+
+ {/* Nodes & Servers */}
+
+
+
+ {lstrings.settings_debug_nodes_servers}
+
+
+
+ {showNodesAndServers && wallets.length === 0 ? (
+
+ {lstrings.settings_debug_no_wallets}
+
+ ) : null}
+ {showNodesAndServers
+ ? wallets.map(wallet => (
+
+ ))
+ : null}
+
+
+ {/* Engine dataDump */}
+
+
+
+ {lstrings.settings_debug_engine_dump}
+
+
+
+ {showDataDump && wallets.length === 0 ? (
+
+ {lstrings.settings_debug_no_wallets}
+
+ ) : null}
+ {showDataDump
+ ? wallets.map(wallet => (
+
+ ))
+ : null}
+
+
+ {/* Log Viewer */}
+
+
+
+ {lstrings.settings_debug_logs}
+
+
+
+ {showLogs ? (
+ <>
+
+
+
+
+ {lstrings.settings_debug_info_log}
+
+
+
+ {showInfoLog ? (
+
+ {__DEV__ ? (
+
+ {lstrings.settings_debug_info_log_dev}
+
+ ) : logsInfo.trim() !== '' ? (
+
+ {logsInfo}
+
+ ) : (
+
+ {lstrings.settings_debug_no_logs}
+
+ )}
+
+ ) : null}
+
+
+
+ {lstrings.settings_debug_activity_log}
+
+
+
+ {showActivityLog ? (
+
+ {logsActivity.trim() !== '' ? (
+
+ {logsActivity}
+
+ ) : (
+
+ {lstrings.settings_debug_no_logs}
+
+ )}
+
+ ) : null}
+ >
+ ) : null}
+
+
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Sub-components (avoid inline arrow fns in JSX handlers)
+// ---------------------------------------------------------------------------
+
+const NodesWalletSection: React.FC = props => {
+ const { wallet, dumpResult, isExpanded, isLoading, onToggle, onLongPress } =
+ props
+ const theme = useTheme()
+ const styles = getStyles(theme)
+
+ const handleToggle = useHandler(() => {
+ onToggle(wallet.id)
+ })
+
+ const handleLongPress = useHandler(() => {
+ onLongPress(wallet.id)
+ })
+
+ const walletLabel = getWalletLabel(wallet)
+
+ const data = dumpResult?.dump?.data
+
+ return (
+
+
+
+ {walletLabel}
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ {isExpanded && data != null ? (
+
+
+ {lstrings.settings_debug_defaults}
+
+
+
+ {JSON.stringify(data.defaultServers ?? {}, null, 2)}
+
+
+
+
+ {lstrings.settings_debug_info_servers}
+
+
+
+ {JSON.stringify(data.infoServerServers ?? {}, null, 2)}
+
+
+
+
+ {lstrings.settings_debug_custom_servers}
+
+
+
+ {JSON.stringify(data.customServers ?? {}, null, 2)}
+
+
+
+
+ {lstrings.settings_debug_user_settings}
+
+
+
+ {JSON.stringify(
+ wallet.currencyConfig.userSettings ?? {},
+ null,
+ 2
+ )}
+
+
+
+ {data.networkConfig != null ? (
+ <>
+
+ {lstrings.settings_debug_network_config}
+
+
+
+ {JSON.stringify(data.networkConfig, null, 2)}
+
+
+ >
+ ) : null}
+
+ ) : null}
+ {isExpanded && dumpResult?.error != null ? (
+
+ {dumpResult.error}
+
+ ) : null}
+
+ )
+}
+
+const DumpWalletRow: React.FC = props => {
+ const { wallet, dumpResult, isExpanded, isLoading, onPress, onLongPress } =
+ props
+ const theme = useTheme()
+ const styles = getStyles(theme)
+
+ const handlePress = useHandler(() => {
+ onPress(wallet.id)
+ })
+
+ const handleLongPress = useHandler(() => {
+ onLongPress(wallet.id)
+ })
+
+ const walletLabel = getWalletLabel(wallet)
+
+ return (
+
+
+
+ {walletLabel}
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ {isExpanded && dumpResult?.dump != null ? (
+
+
+ {JSON.stringify(dumpResult.dump, null, 2)}
+
+
+ ) : null}
+ {isExpanded && dumpResult?.error != null ? (
+
+ {dumpResult.error}
+
+ ) : null}
+
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Utilities
+// ---------------------------------------------------------------------------
+
+const getWalletLabel = (wallet: EdgeCurrencyWallet): string =>
+ `${wallet.name ?? wallet.currencyInfo.currencyCode} (${
+ wallet.currencyInfo.pluginId
+ })`
+
+// ---------------------------------------------------------------------------
+// Styles
+// ---------------------------------------------------------------------------
+
+const getStyles = cacheStyles((theme: Theme) => ({
+ hintText: {
+ fontSize: theme.rem(0.7),
+ color: theme.deactivatedText,
+ textAlign: 'center',
+ paddingVertical: theme.rem(0.5)
+ },
+ sectionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: theme.rem(0.75)
+ },
+ sectionTitle: {
+ fontSize: theme.rem(1),
+ fontFamily: theme.fontFaceMedium
+ },
+ walletHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: theme.rem(0.5),
+ paddingHorizontal: theme.rem(0.75)
+ },
+ walletTitle: {
+ fontSize: theme.rem(0.875),
+ fontFamily: theme.fontFaceMedium,
+ flexShrink: 1,
+ marginRight: theme.rem(0.5)
+ },
+ walletContent: {
+ paddingHorizontal: theme.rem(0.75),
+ paddingBottom: theme.rem(0.5)
+ },
+ subLabel: {
+ fontSize: theme.rem(0.8),
+ fontFamily: theme.fontFaceBold,
+ marginTop: theme.rem(0.5),
+ marginBottom: theme.rem(0.25)
+ },
+ jsonBox: {
+ maxHeight: theme.rem(12),
+ marginBottom: theme.rem(0.25),
+ padding: theme.rem(0.5),
+ backgroundColor: theme.tileBackground,
+ borderRadius: theme.rem(0.5),
+ borderWidth: 1,
+ borderColor: theme.lineDivider
+ },
+ logText: {
+ fontSize: theme.rem(0.5),
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ color: theme.primaryText
+ },
+ logSubHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: theme.rem(0.5),
+ paddingHorizontal: theme.rem(0.75)
+ },
+ logSectionLabel: {
+ fontSize: theme.rem(0.9),
+ fontFamily: theme.fontFaceBold
+ },
+ logBox: {
+ maxHeight: theme.rem(20),
+ marginHorizontal: theme.rem(0.5),
+ marginBottom: theme.rem(0.5),
+ padding: theme.rem(0.5),
+ backgroundColor: theme.tileBackground,
+ borderRadius: theme.rem(0.5),
+ borderWidth: 1,
+ borderColor: theme.lineDivider
+ },
+ logEmptyText: {
+ fontSize: theme.rem(0.75),
+ color: theme.deactivatedText
+ },
+ errorText: {
+ fontSize: theme.rem(0.65),
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ color: theme.dangerText,
+ padding: theme.rem(0.5)
+ },
+ emptyText: {
+ padding: theme.rem(0.75)
+ }
+}))
diff --git a/src/components/scenes/SettingsScene.tsx b/src/components/scenes/SettingsScene.tsx
index 6f87e29bd85..fe8fb3f3540 100644
--- a/src/components/scenes/SettingsScene.tsx
+++ b/src/components/scenes/SettingsScene.tsx
@@ -416,6 +416,10 @@ export const SettingsScene: React.FC = props => {
navigation.navigate('swapSettings')
})
+ const handleOpenDebugSettings = useHandler((): void => {
+ navigation.navigate('debugSettings')
+ })
+
const handleSpendingLimits = useHandler(async (): Promise => {
if (await hasLock()) return
navigation.navigate('spendingLimits')
@@ -732,6 +736,12 @@ export const SettingsScene: React.FC = props => {
}}
/>
)}
+ {developerModeOn && (
+
+ )}
)}
diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts
index 5f6b1d5a827..9b1c1693cbf 100644
--- a/src/locales/en_US.ts
+++ b/src/locales/en_US.ts
@@ -513,6 +513,23 @@ const strings = {
settings_button_change_password: 'Change Password',
settings_button_change_username: 'Change Username',
settings_developer_mode: 'Developer Mode',
+ settings_debug_title: 'Debug',
+ settings_debug_nodes_servers: 'Nodes & Servers',
+ settings_debug_engine_dump: 'Engine dataDump',
+ settings_debug_logs: 'Info/Activity Logs',
+ settings_debug_refresh_logs: 'Refresh',
+ settings_debug_defaults: 'Defaults',
+ settings_debug_info_servers: 'Info-Server Added',
+ settings_debug_custom_servers: 'User Added',
+ settings_debug_user_settings: 'User Settings',
+ settings_debug_network_config: 'Network Config',
+ settings_debug_no_wallets: 'No wallets available',
+ settings_debug_info_log: 'Info Log',
+ settings_debug_activity_log: 'Activity Log',
+ settings_debug_no_logs: 'No logs available',
+ settings_debug_info_log_dev: 'Not available in __DEV__',
+ settings_debug_long_press_hint: 'Long press any header to copy',
+ settings_debug_copied_1s: 'Copied: %1$s',
settings_verbose_logging: 'Verbose Logging',
settings_theme: 'Theme',
settings_theme_light: 'Light',
diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json
index 2ce36adcf86..5aabb0ecdb6 100644
--- a/src/locales/strings/enUS.json
+++ b/src/locales/strings/enUS.json
@@ -367,6 +367,23 @@
"settings_button_change_password": "Change Password",
"settings_button_change_username": "Change Username",
"settings_developer_mode": "Developer Mode",
+ "settings_debug_title": "Debug",
+ "settings_debug_nodes_servers": "Nodes & Servers",
+ "settings_debug_engine_dump": "Engine dataDump",
+ "settings_debug_logs": "Info/Activity Logs",
+ "settings_debug_refresh_logs": "Refresh",
+ "settings_debug_defaults": "Defaults",
+ "settings_debug_info_servers": "Info-Server Added",
+ "settings_debug_custom_servers": "User Added",
+ "settings_debug_user_settings": "User Settings",
+ "settings_debug_network_config": "Network Config",
+ "settings_debug_no_wallets": "No wallets available",
+ "settings_debug_info_log": "Info Log",
+ "settings_debug_activity_log": "Activity Log",
+ "settings_debug_no_logs": "No logs available",
+ "settings_debug_info_log_dev": "Not available in __DEV__",
+ "settings_debug_long_press_hint": "Long press any header to copy",
+ "settings_debug_copied_1s": "Copied: %1$s",
"settings_verbose_logging": "Verbose Logging",
"settings_theme": "Theme",
"settings_theme_light": "Light",
diff --git a/src/types/routerTypes.tsx b/src/types/routerTypes.tsx
index 9c53eddc19f..549cd757894 100644
--- a/src/types/routerTypes.tsx
+++ b/src/types/routerTypes.tsx
@@ -181,6 +181,7 @@ export type EdgeAppStackParamList = {} & {
createWalletSelectCryptoNewAccount: CreateWalletSelectCryptoParams
currencyNotificationSettings: CurrencyNotificationParams
currencySettings: CurrencySettingsParams
+ debugSettings: undefined
defaultFiatSetting: undefined
duressModeHowTo: undefined
duressModeSetting: undefined