commit df35367355d048793d01281b512ee02dd89fd89a
parent a8d420a0465648a4ec0236eb03d9bf6ecf43f134
Author: triesap <tyson@radroots.org>
Date: Sun, 2 Aug 2026 19:05:58 +0000
core(accounts): implement selected account persistence
- validate selection targets against the saved account registry
- persist the selected public identity before snapshot publication
- keep signer and active-session state independent from selection
- reject missing targets without changing the current snapshot
Diffstat:
3 files changed, 62 insertions(+), 2 deletions(-)
diff --git a/core/crates/application/src/accounts.rs b/core/crates/application/src/accounts.rs
@@ -42,6 +42,24 @@ impl GenerateAccountReceipt {
}
impl AppCore {
+ /// Persists and publishes a saved account selection without activating it.
+ ///
+ /// # Errors
+ ///
+ /// Returns a safe account, persistence, or application-state error.
+ pub fn select_account(
+ &self,
+ public_key: PublicKey,
+ accounts: &(impl AccountRepository + ?Sized),
+ app_state: &(impl AppStateRepository + ?Sized),
+ ) -> Result<crate::AppSnapshot, SafeError> {
+ if accounts.find_account(public_key)?.is_none() {
+ return Err(account_not_found());
+ }
+ app_state.save_selected_account(Some(public_key))?;
+ self.apply_transition(StateTransition::Select(public_key))
+ }
+
/// Generates, stores, and selects one local Nostr account without activating it.
///
/// # Errors
@@ -726,4 +744,36 @@ mod tests {
assert!(!format!("{pending:?}").contains("nsec1"));
assert!(core.snapshot().accounts().is_empty());
}
+
+ #[test]
+ fn select_account_persists_existing_choice_without_activating() {
+ let core = AppCore::in_memory(RelayConfiguration::default());
+ let accounts = InMemoryAccountRepository::default();
+ let secrets = InMemorySecretStore::default();
+ let journal = InMemoryOperationJournal::default();
+ core.bootstrap().expect("bootstrap");
+ let first = core
+ .generate_account(&accounts, &accounts, &secrets, &journal, &FixedClock)
+ .expect("first")
+ .account()
+ .public_key();
+ core.generate_account(&accounts, &accounts, &secrets, &journal, &FixedClock)
+ .expect("second");
+
+ let selected = core
+ .select_account(first, &accounts, &accounts)
+ .expect("select first");
+ assert_eq!(selected.selected_account(), Some(first));
+ assert_eq!(selected.session(), SessionState::SignedOut);
+ assert!(selected.active_account().is_none());
+ assert_eq!(
+ accounts.load_selected_account().expect("saved"),
+ Some(first)
+ );
+ let missing = core
+ .select_account(PublicKey::from_bytes([0xff; 32]), &accounts, &accounts)
+ .expect_err("missing account");
+ assert_eq!(missing.code(), SafeErrorCode::AccountNotFound);
+ assert_eq!(core.snapshot(), selected);
+ }
}
diff --git a/core/crates/storage/src/application_adapter.rs b/core/crates/storage/src/application_adapter.rs
@@ -4,7 +4,7 @@ use radroots_studio_application::{
AppCore, AppSnapshot, Clock, GenerateAccountReceipt, ImportAccountReceipt, RelayConfiguration,
SecretStore,
};
-use radroots_studio_domain::{SafeError, SecretKeyInput};
+use radroots_studio_domain::{PublicKey, SafeError, SecretKeyInput};
use crate::Database;
@@ -88,6 +88,16 @@ impl PersistentAppCore {
)
}
+ /// Persists and publishes one saved-account selection without activation.
+ ///
+ /// # Errors
+ ///
+ /// Returns a safe account, storage, or application-state error.
+ pub fn select_account(&self, public_key: PublicKey) -> Result<AppSnapshot, SafeError> {
+ self.core
+ .select_account(public_key, &self.database, &self.database)
+ }
+
#[must_use]
pub const fn core(&self) -> &AppCore {
&self.core
diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md
@@ -505,7 +505,7 @@ handoff commit sequence.
### RCLD-07
-- [ ] 31. Implement select account command.
+- [x] 31. Implement select account command.
- [ ] 32. Implement activate account with safe replacement ordering.
- [ ] 33. Implement sign out command.
- [ ] 34. Implement revision-bound removal request/confirmation flow.