-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathapp_settings.go
More file actions
296 lines (258 loc) · 10.1 KB
/
Copy pathapp_settings.go
File metadata and controls
296 lines (258 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Copyright 2025 HOLOGRAM Project. All rights reserved.
// Settings Management - Extracted from app.go for organization
// Session 87: Domain splitting
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
)
// Settings that should be persisted to disk
// Not all settings need persistence - only user-configured values
var persistedSettingKeys = []string{
"daemon_endpoint",
"network",
"min_rating",
"block_malware",
"show_nsfw",
"auto_connect_ws",
"gnomon_enabled",
"integrated_wallet",
"allow_github_check",
"wizard_complete",
"dev_support_enabled",
"epoch_enabled",
"hide_balance",
"hide_address",
"avatar_hidden",
"privacy_mode",
"signal_dark",
"active_ring_member_set",
}
// Settings Functions
func (a *App) GetSetting(key string) interface{} {
if val, ok := a.settings[key]; ok {
return val
}
return nil
}
// GetAllSettings returns all settings for frontend sync
func (a *App) GetAllSettings() map[string]interface{} {
return a.settings
}
func (a *App) SetSetting(settingJSON string) map[string]interface{} {
var data map[string]interface{}
if err := json.Unmarshal([]byte(settingJSON), &data); err != nil {
return map[string]interface{}{
"success": false,
"error": err.Error(),
}
}
for k, v := range data {
a.settings[k] = v
log.Printf("[Settings] Updated: %s = %v", k, v)
}
// Persist settings to disk
a.saveSettings()
return map[string]interface{}{
"success": true,
"message": "Settings updated",
}
}
// saveSettings persists user-configured settings to disk
// Settings are saved to ~/.dero/hologram/datashards/settings/settings.json
func (a *App) saveSettings() {
configDir := filepath.Join(getDatashardsDir(), "settings")
if err := os.MkdirAll(configDir, 0700); err != nil {
log.Printf("[Settings] Failed to create settings directory: %v", err)
return
}
// Only persist specific settings, not all in-memory values
toSave := make(map[string]interface{})
for _, key := range persistedSettingKeys {
if val, ok := a.settings[key]; ok {
toSave[key] = val
}
}
data, err := json.MarshalIndent(toSave, "", " ")
if err != nil {
log.Printf("[Settings] Failed to marshal settings: %v", err)
return
}
settingsFile := filepath.Join(configDir, "settings.json")
if err := os.WriteFile(settingsFile, data, 0600); err != nil {
log.Printf("[Settings] Failed to save settings: %v", err)
} else {
log.Printf("[Settings] Saved settings to %s", settingsFile)
}
}
// loadSettings loads persisted settings from disk and merges with defaults
// Call this during app startup after defaults are set
func (a *App) loadSettings() {
settingsFile := filepath.Join(getDatashardsDir(), "settings", "settings.json")
data, err := os.ReadFile(settingsFile)
if err != nil {
// No settings file yet - this is normal on first run
if !os.IsNotExist(err) {
log.Printf("[Settings] Failed to read settings file: %v", err)
}
return
}
var loaded map[string]interface{}
if err := json.Unmarshal(data, &loaded); err != nil {
log.Printf("[Settings] Failed to parse settings file: %v", err)
return
}
// Merge loaded settings into current settings (overwriting defaults)
for key, val := range loaded {
a.settings[key] = val
log.Printf("[Settings] Loaded from disk: %s = %v", key, val)
}
log.Printf("[Settings] Loaded %d settings from %s", len(loaded), settingsFile)
}
// isLocalhostEndpoint returns true if the endpoint points to the local machine.
// Remote endpoints (LAN IPs, hostnames, etc.) must never be auto-corrected.
func isLocalhostEndpoint(endpoint string) bool {
lower := strings.ToLower(endpoint)
return strings.Contains(lower, "127.0.0.1") ||
strings.Contains(lower, "localhost") ||
strings.Contains(lower, "::1")
}
// isDerivedEndpoint reports whether an endpoint is one HOLOGRAM generates itself —
// empty, or localhost on a network's own default RPC port.
//
// Several places recompute the endpoint from the network mode and write the result back
// (network switch, and three reconciliation branches at startup). That is correct for an
// endpoint HOLOGRAM derived — a stale simulator :20000 left over on a mainnet restart has
// to be corrected — and destructive for one the user typed, which is how a node on any
// other port silently lost its configuration on every network switch.
//
// Anything else is treated as the user's and left alone, including a remote host and a
// localhost node on a non-default port. Deliberately conservative: an endpoint we cannot
// prove we generated is not ours to overwrite.
func isDerivedEndpoint(endpoint string) bool {
if strings.TrimSpace(endpoint) == "" {
return true
}
if !isLocalhostEndpoint(endpoint) {
return false
}
port := inferRPCPortFromEndpoint(endpoint)
for _, mode := range []NetworkMode{NetworkMainnet, NetworkSimulator} {
if port == GetNetworkConfig(mode).RPCPort {
return true
}
}
return false
}
// reconcileDaemonEndpoint ensures daemon_endpoint, daemonClient, and network are
// consistent after loading persisted settings. This handles the case where a user
// was previously on simulator (port 20000) but is now restarting on mainnet — the
// persisted daemon_endpoint would be stale and cause Gnomon/wallet/EPOCH to fail.
//
// Remote (non-localhost) endpoints are always preserved as-is; only localhost
// endpoints are subject to port correction so that simulator ↔ mainnet switches
// don't leave stale port numbers behind.
func (a *App) reconcileDaemonEndpoint() {
loadedEndpoint, _ := a.settings["daemon_endpoint"].(string)
loadedNetwork, _ := a.settings["network"].(string)
// Step 1: Sync daemonClient with the loaded endpoint so the connection test
// hits whatever the user had configured (not the hardcoded default).
if loadedEndpoint != "" {
a.daemonClient.SetEndpoint(loadedEndpoint)
}
// Step 2: Try to reach the daemon at the loaded endpoint.
// If it responds, infer network from GetInfo() (daemon "network" field first, then height).
if err := a.daemonClient.TestConnection(); err == nil {
info, infoErr := a.daemonClient.GetInfo()
if infoErr == nil {
if inferredMode, ok := inferNetworkModeFromDaemonInfo(info, loadedEndpoint); ok {
inferredNetwork := string(inferredMode)
if inferredNetwork != loadedNetwork {
// Network mismatch — update network label and, for localhost
// endpoints, correct the port to match the detected network.
netConfig := GetNetworkConfig(inferredMode)
correctEndpoint := loadedEndpoint
if isDerivedEndpoint(loadedEndpoint) {
correctEndpoint = fmt.Sprintf("http://127.0.0.1:%d", netConfig.RPCPort)
}
log.Printf("[Settings] Network reconciliation: persisted=%s, detected=%s — correcting network label (endpoint: %s → %s)",
loadedNetwork, inferredNetwork, loadedEndpoint, correctEndpoint)
a.settings["network"] = inferredNetwork
a.settings["daemon_endpoint"] = correctEndpoint
a.daemonClient.SetEndpoint(correctEndpoint)
nodeManager.Lock()
nodeManager.networkMode = inferredMode
nodeManager.rpcPort = netConfig.RPCPort
nodeManager.p2pPort = netConfig.P2PPort
nodeManager.getworkPort = netConfig.GetWorkPort
nodeManager.Unlock()
a.saveSettings()
} else {
// Connection succeeded and network matches — nothing to correct.
log.Printf("[Settings] Daemon reachable at %s, network=%s — no correction needed", loadedEndpoint, loadedNetwork)
}
return
}
}
// Connected but couldn't determine network — leave endpoint as-is.
return
}
// Step 3: Loaded endpoint is unreachable.
// Only attempt fallback corrections for endpoints HOLOGRAM derived itself. A remote
// node and a localhost node on a custom port are both the user's configuration, and
// are preserved so they can fix connectivity on their end rather than finding their
// endpoint silently replaced.
if !isDerivedEndpoint(loadedEndpoint) {
log.Printf("[Settings] User-set endpoint %s is currently unreachable — preserving for user to reconnect", loadedEndpoint)
return
}
// If persisted network is simulator, the daemon was likely a child process
// from a previous session that is no longer running. Try falling back to
// mainnet so the user isn't stuck on a dead endpoint.
if loadedNetwork == "simulator" {
mainnetConfig := GetNetworkConfig(NetworkMainnet)
mainnetEndpoint := fmt.Sprintf("http://127.0.0.1:%d", mainnetConfig.RPCPort)
a.daemonClient.SetEndpoint(mainnetEndpoint)
if err := a.daemonClient.TestConnection(); err == nil {
log.Printf("[Settings] Simulator unreachable — falling back to mainnet at %s", mainnetEndpoint)
a.settings["network"] = "mainnet"
a.settings["daemon_endpoint"] = mainnetEndpoint
nodeManager.Lock()
nodeManager.networkMode = NetworkMainnet
nodeManager.rpcPort = mainnetConfig.RPCPort
nodeManager.p2pPort = mainnetConfig.P2PPort
nodeManager.getworkPort = mainnetConfig.GetWorkPort
nodeManager.Unlock()
a.saveSettings()
return
}
// Neither simulator nor mainnet reachable — restore the loaded endpoint
// so settings stay internally consistent.
a.daemonClient.SetEndpoint(loadedEndpoint)
}
// For endpoints HOLOGRAM derived only: if the port doesn't match the persisted
// network's expected port (e.g. user switched network label without updating
// endpoint), correct the localhost port. A user-set endpoint never reaches here —
// the guard above returns first — and the test asserts that.
if loadedNetwork != "" && isDerivedEndpoint(loadedEndpoint) {
netConfig := GetNetworkConfig(NetworkMode(loadedNetwork))
expectedEndpoint := fmt.Sprintf("http://127.0.0.1:%d", netConfig.RPCPort)
if loadedEndpoint != expectedEndpoint {
log.Printf("[Settings] Localhost endpoint/network mismatch: endpoint=%s but network=%s (expected %s) — correcting",
loadedEndpoint, loadedNetwork, expectedEndpoint)
a.settings["daemon_endpoint"] = expectedEndpoint
a.daemonClient.SetEndpoint(expectedEndpoint)
nodeManager.Lock()
nodeManager.networkMode = NetworkMode(loadedNetwork)
nodeManager.rpcPort = netConfig.RPCPort
nodeManager.p2pPort = netConfig.P2PPort
nodeManager.getworkPort = netConfig.GetWorkPort
nodeManager.Unlock()
a.saveSettings()
}
}
}