commit b0b194d8ca663be54e2f9a1d6a20a34624dec2c4
parent c68f93ef61bea6916b2cbb836ace420430f25183
Author: triesap <tyson@radroots.org>
Date: Mon, 2 Feb 2026 03:44:14 +0000
app: seed default relays and profile data
- add default relay parsing with env fallback
- store relays in setup state and tests
- add profile seed datastore helper
- persist profile seed and nip05 request on finalize
Diffstat:
6 files changed, 258 insertions(+), 3 deletions(-)
diff --git a/app/src/app.rs b/app/src/app.rs
@@ -41,6 +41,7 @@ use crate::{
app_datastore_clear_setup_draft,
app_datastore_read_state,
app_datastore_read_setup_draft,
+ app_datastore_write_profile_seed,
app_datastore_write_setup_draft,
app_keystore_nostr_ensure_key,
app_state_notifications_permission_value,
@@ -59,6 +60,7 @@ use crate::{
RadrootsAppNotifications,
RadrootsAppLogsPage,
RadrootsAppKeystoreError,
+ RadrootsAppProfileSeed,
RadrootsAppRole,
RadrootsAppSettingsPage,
RadrootsAppSetupDraft,
@@ -341,6 +343,8 @@ fn SetupPage() -> impl IntoView {
if matches!(current_step, RadrootsAppSetupStep::Eula) {
let key_choice = setup_key_choice.get();
let nostr_key_add = nostr_key_add.get();
+ let profile_name = profile_name.get();
+ let profile_nip05 = profile_nip05.get();
let eula_date = app_setup_eula_date();
let setup_required = setup_required.clone();
let backends = backends.clone();
@@ -392,11 +396,40 @@ fn SetupPage() -> impl IntoView {
}
},
};
+ let nip05_key = if profile_nip05 {
+ let profile_name = profile_name.trim();
+ if profile_name.is_empty() {
+ None
+ } else {
+ Some(profile_name.to_string())
+ }
+ } else {
+ None
+ };
+ if !profile_name.trim().is_empty() {
+ let profile_seed = RadrootsAppProfileSeed {
+ public_key: active_key.clone(),
+ name: profile_name.trim().to_string(),
+ display_name: Some(profile_name.trim().to_string()),
+ nip05_request: profile_nip05,
+ };
+ if let Err(err) = app_datastore_write_profile_seed(
+ datastore.as_ref(),
+ &key_maps,
+ &profile_seed,
+ )
+ .await
+ {
+ let _ = app_log_error_emit(&err);
+ return;
+ }
+ }
if let Err(err) = app_setup_finalize_with_key(
datastore.as_ref(),
&key_maps,
active_key,
eula_date,
+ nip05_key,
)
.await
{
diff --git a/app/src/bootstrap.rs b/app/src/bootstrap.rs
@@ -6,10 +6,12 @@ use radroots_app_core::notifications::RadrootsClientNotificationsPermission;
use crate::{
app_datastore_obj_key_state,
app_datastore_obj_key_setup_draft,
+ app_datastore_param_key,
app_log_debug_emit,
app_state_record_new,
app_state_record_validate,
app_state_timestamp_ms,
+ RadrootsAppProfileSeed,
RadrootsAppState,
RadrootsAppSetupDraft,
RadrootsAppStateError,
@@ -174,6 +176,22 @@ pub async fn app_datastore_clear_setup_draft<T: RadrootsClientDatastore>(
}
}
+pub async fn app_datastore_write_profile_seed<T: RadrootsClientDatastore>(
+ datastore: &T,
+ key_maps: &RadrootsAppKeyMapConfig,
+ profile: &RadrootsAppProfileSeed,
+) -> RadrootsAppInitResult<RadrootsAppProfileSeed> {
+ let param = app_datastore_param_key(key_maps, "nostr_profile")
+ .map_err(RadrootsAppInitError::Config)?;
+ let key = param(&profile.public_key);
+ let stored = datastore
+ .set_obj(&key, profile)
+ .await
+ .map_err(RadrootsAppInitError::Datastore)?;
+ let _ = app_log_debug_emit("log.app.bootstrap.profile", "write", Some(key));
+ Ok(stored)
+}
+
pub async fn app_state_set_notifications_permission<T: RadrootsClientDatastore>(
datastore: &T,
key_maps: &RadrootsAppKeyMapConfig,
@@ -212,6 +230,7 @@ mod tests {
app_datastore_read_setup_draft,
app_datastore_update_state,
app_datastore_write_setup_draft,
+ app_datastore_write_profile_seed,
app_state_set_notifications_permission,
app_state_set_notifications_permission_value,
app_state_notifications_permission_value,
@@ -220,6 +239,7 @@ mod tests {
use crate::{
app_key_maps_default,
RadrootsAppInitError,
+ RadrootsAppProfileSeed,
RadrootsAppRole,
RadrootsAppState,
RadrootsAppStateError,
@@ -245,6 +265,10 @@ mod tests {
draft: RefCell<Option<RadrootsAppSetupDraft>>,
}
+ struct ProfileSeedDatastore {
+ profile: RefCell<Option<RadrootsAppProfileSeed>>,
+ }
+
#[async_trait(?Send)]
impl RadrootsClientDatastore for SetupDraftDatastore {
fn get_config(&self) -> RadrootsClientIdbConfig {
@@ -371,6 +395,132 @@ mod tests {
}
}
+ #[async_trait(?Send)]
+ impl RadrootsClientDatastore for ProfileSeedDatastore {
+ fn get_config(&self) -> RadrootsClientIdbConfig {
+ IDB_CONFIG_DATASTORE
+ }
+
+ fn get_store_id(&self) -> &str {
+ "test"
+ }
+
+ async fn init(&self) -> RadrootsClientDatastoreResult<()> {
+ Ok(())
+ }
+
+ async fn set(&self, _key: &str, _value: &str) -> RadrootsClientDatastoreResult<String> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn get(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn set_obj<T>(
+ &self,
+ _key: &str,
+ value: &T,
+ ) -> RadrootsClientDatastoreResult<T>
+ where
+ T: Serialize + DeserializeOwned + Clone,
+ {
+ let encoded = serde_json::to_string(value)
+ .map_err(|_| RadrootsClientDatastoreError::IdbUndefined)?;
+ if let Ok(parsed) = serde_json::from_str::<RadrootsAppProfileSeed>(&encoded) {
+ *self.profile.borrow_mut() = Some(parsed);
+ return Ok(value.clone());
+ }
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn update_obj<T>(
+ &self,
+ _key: &str,
+ _value: &T,
+ ) -> RadrootsClientDatastoreResult<T>
+ where
+ T: Serialize + DeserializeOwned + Clone,
+ {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn get_obj<T>(&self, _key: &str) -> RadrootsClientDatastoreResult<T>
+ where
+ T: DeserializeOwned,
+ {
+ if let Some(profile) = self.profile.borrow().as_ref() {
+ let encoded = serde_json::to_string(profile)
+ .map_err(|_| RadrootsClientDatastoreError::NoResult)?;
+ return serde_json::from_str(&encoded)
+ .map_err(|_| RadrootsClientDatastoreError::NoResult);
+ }
+ Err(RadrootsClientDatastoreError::NoResult)
+ }
+
+ async fn del_obj(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
+ *self.profile.borrow_mut() = None;
+ Ok("cleared".to_string())
+ }
+
+ async fn del(&self, _key: &str) -> RadrootsClientDatastoreResult<String> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn del_pref(&self, _key_prefix: &str) -> RadrootsClientDatastoreResult<Vec<String>> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn set_param(
+ &self,
+ _key: &str,
+ _key_param: &str,
+ _value: &str,
+ ) -> RadrootsClientDatastoreResult<String> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn get_param(
+ &self,
+ _key: &str,
+ _key_param: &str,
+ ) -> RadrootsClientDatastoreResult<String> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn keys(&self) -> RadrootsClientDatastoreResult<Vec<String>> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn entries(&self) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn entries_pref(
+ &self,
+ _key_prefix: &str,
+ ) -> RadrootsClientDatastoreResult<RadrootsClientDatastoreEntries> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn reset(&self) -> RadrootsClientDatastoreResult<()> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn export_backup(
+ &self,
+ ) -> RadrootsClientDatastoreResult<RadrootsClientBackupDatastorePayload> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+
+ async fn import_backup(
+ &self,
+ _payload: RadrootsClientBackupDatastorePayload,
+ ) -> RadrootsClientDatastoreResult<()> {
+ Err(RadrootsClientDatastoreError::IdbUndefined)
+ }
+ }
+
struct TestDatastore {
state: Option<RadrootsAppState>,
record: RefCell<Option<RadrootsAppStateRecord>>,
@@ -726,4 +876,27 @@ mod tests {
.expect("read draft");
assert!(loaded.is_none());
}
+
+ #[test]
+ fn profile_seed_write_persists_data() {
+ let datastore = ProfileSeedDatastore {
+ profile: RefCell::new(None),
+ };
+ let key_maps = app_key_maps_default();
+ let profile = RadrootsAppProfileSeed {
+ public_key: "pub".to_string(),
+ name: "radroots".to_string(),
+ display_name: Some("Radroots".to_string()),
+ nip05_request: true,
+ };
+ let stored = futures::executor::block_on(app_datastore_write_profile_seed(
+ &datastore,
+ &key_maps,
+ &profile,
+ ))
+ .expect("profile seed");
+ assert_eq!(stored, profile);
+ let stored_profile = datastore.profile.borrow().clone();
+ assert_eq!(stored_profile, Some(profile));
+ }
}
diff --git a/app/src/config.rs b/app/src/config.rs
@@ -1,6 +1,6 @@
#![forbid(unsafe_code)]
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, BTreeSet};
use radroots_app_core::idb::{
RadrootsClientIdbConfig,
@@ -244,6 +244,27 @@ pub fn app_assets_geocoder_db_url(config: &RadrootsAppConfig) -> Option<&str> {
config.assets.geocoder_db_url.as_deref()
}
+const APP_DEFAULT_RELAY_FALLBACK: &str = "ws://localhost:8080";
+
+pub fn app_default_relays() -> Vec<String> {
+ let raw = option_env!("VITE_PUBLIC_DEFAULT_RELAYS")
+ .or(option_env!("VITE_PUBLIC_RADROOTS_RELAY"))
+ .unwrap_or(APP_DEFAULT_RELAY_FALLBACK);
+ let mut seen = BTreeSet::new();
+ let mut relays = Vec::new();
+ for relay in raw.split(',') {
+ let relay = relay.trim();
+ if relay.is_empty() || !seen.insert(relay.to_string()) {
+ continue;
+ }
+ relays.push(relay.to_string());
+ }
+ if relays.is_empty() {
+ relays.push(APP_DEFAULT_RELAY_FALLBACK.to_string());
+ }
+ relays
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadrootsAppConfig {
pub datastore: RadrootsAppDatastoreConfig,
@@ -289,6 +310,7 @@ mod tests {
use super::{
app_config_default,
app_config_from_env,
+ app_default_relays,
app_datastore_param_nostr_profile,
app_datastore_param_log_entry,
app_datastore_param_radroots_profile,
@@ -337,6 +359,13 @@ mod tests {
}
#[test]
+ fn default_relays_are_non_empty() {
+ let relays = app_default_relays();
+ assert!(!relays.is_empty());
+ assert!(relays.iter().all(|relay| !relay.trim().is_empty()));
+ }
+
+ #[test]
fn app_config_helpers_return_defaults() {
let config = app_config_default();
let from_env = app_config_from_env();
diff --git a/app/src/data.rs b/app/src/data.rs
@@ -22,6 +22,7 @@ pub struct RadrootsAppState {
pub eula_date: String,
pub eula_version: String,
pub eula_hash: String,
+ pub relays: Vec<String>,
pub nip05_key: Option<String>,
pub notifications_permission: Option<String>,
}
@@ -34,6 +35,7 @@ impl Default for RadrootsAppState {
eula_date: String::new(),
eula_version: String::from("0.1.0"),
eula_hash: String::from("unknown"),
+ relays: Vec::new(),
nip05_key: None,
notifications_permission: None,
}
@@ -48,6 +50,14 @@ pub struct RadrootsAppSetupDraft {
pub nip05_request: Option<bool>,
}
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RadrootsAppProfileSeed {
+ pub public_key: String,
+ pub name: String,
+ pub display_name: Option<String>,
+ pub nip05_request: bool,
+}
+
pub const APP_STATE_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -190,6 +200,7 @@ mod tests {
assert_eq!(data.eula_date, "");
assert_eq!(data.eula_version, "0.1.0");
assert_eq!(data.eula_hash, "unknown");
+ assert!(data.relays.is_empty());
assert!(data.nip05_key.is_none());
assert!(data.notifications_permission.is_none());
}
diff --git a/app/src/lib.rs b/app/src/lib.rs
@@ -27,6 +27,7 @@ pub use bootstrap::{
app_datastore_read_setup_draft,
app_datastore_write_setup_draft,
app_datastore_clear_setup_draft,
+ app_datastore_write_profile_seed,
app_datastore_update_state,
app_state_set_notifications_permission,
app_state_set_notifications_permission_value,
@@ -39,6 +40,7 @@ pub use data::{
app_state_record_new,
app_state_record_validate,
app_state_timestamp_ms,
+ RadrootsAppProfileSeed,
RadrootsAppRole,
RadrootsAppState,
RadrootsAppSetupDraft,
@@ -135,6 +137,7 @@ pub use tangle::{RadrootsAppTangleClient, RadrootsAppTangleClientStub, RadrootsA
pub use config::{
app_config_default,
app_config_from_env,
+ app_default_relays,
app_datastore_key,
app_datastore_key_eula_date,
app_datastore_key_nostr_key,
diff --git a/app/src/setup.rs b/app/src/setup.rs
@@ -12,6 +12,7 @@ use chrono::{SecondsFormat, Utc};
use crate::{
app_datastore_create_state,
app_datastore_key_nostr_key,
+ app_default_relays,
app_keystore_nostr_ensure_key,
app_keystore_nostr_verify_key,
app_log_debug_emit,
@@ -40,6 +41,7 @@ pub fn app_setup_state_new(active_key: String, eula_date: String) -> RadrootsApp
eula_date,
eula_version: String::from("0.1.0"),
eula_hash: String::from("unknown"),
+ relays: app_default_relays(),
nip05_key: None,
notifications_permission: None,
}
@@ -108,7 +110,7 @@ pub async fn app_setup_initialize<T: RadrootsClientDatastore, K: RadrootsClientK
RadrootsAppInitError::Keystore(RadrootsClientKeystoreError::NostrInvalidSecretKey)
}
})?;
- app_setup_finalize_with_key(datastore, key_maps, active_key, app_setup_eula_date()).await
+ app_setup_finalize_with_key(datastore, key_maps, active_key, app_setup_eula_date(), None).await
}
pub async fn app_setup_finalize_with_key<T: RadrootsClientDatastore>(
@@ -116,8 +118,10 @@ pub async fn app_setup_finalize_with_key<T: RadrootsClientDatastore>(
key_maps: &RadrootsAppKeyMapConfig,
active_key: String,
eula_date: String,
+ nip05_key: Option<String>,
) -> RadrootsAppInitResult<RadrootsAppState> {
- let state = app_setup_state_new(active_key.clone(), eula_date);
+ let mut state = app_setup_state_new(active_key.clone(), eula_date);
+ state.nip05_key = nip05_key;
let stored_state = app_datastore_create_state(datastore, key_maps, &state).await?;
let key_name = app_datastore_key_nostr_key(key_maps).map_err(RadrootsAppInitError::Config)?;
datastore
@@ -334,6 +338,7 @@ mod tests {
assert_eq!(state.active_key, "pub");
assert_eq!(state.role, RadrootsAppRole::Public);
assert_eq!(state.eula_date, "2025-01-01T00:00:00Z");
+ assert!(!state.relays.is_empty());
assert!(state.nip05_key.is_none());
assert!(state.notifications_permission.is_none());
}
@@ -456,6 +461,7 @@ mod tests {
&key_maps,
"pub".to_string(),
"2025-01-01T00:00:00Z".to_string(),
+ None,
))
.expect("finalize");
assert_eq!(state.active_key, "pub");