commit 67da82d341870391792bcc700a34049edd7aaace
parent ee607e0bbe66361a70c32146c785870a9bd912ca
Author: triesap <tyson@radroots.org>
Date: Mon, 20 Apr 2026 17:20:55 +0000
add typed account resolution surfaces
Diffstat:
14 files changed, 336 insertions(+), 113 deletions(-)
diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs
@@ -46,7 +46,8 @@ pub fn report(
) -> Result<CommandOutput, RuntimeError> {
let mut checks = Vec::new();
checks.push(config_check(config));
- checks.push(account_check(config)?);
+ let account_resolution = crate::runtime::accounts::resolve_account_resolution(config)?;
+ checks.push(account_check(config, &account_resolution)?);
checks.push(relay_check(config));
let signer = resolve_signer_status(config);
@@ -72,6 +73,7 @@ pub fn report(
let view = DoctorView {
ok: severity == DoctorSeverity::Ok,
state: severity.status().to_owned(),
+ account_resolution: crate::runtime::accounts::account_resolution_view(&account_resolution),
checks: checks.into_iter().map(|check| check.view).collect(),
source: doctor_source(config),
actions,
@@ -112,7 +114,10 @@ fn config_check(config: &RuntimeConfig) -> EvaluatedCheck {
}
}
-fn account_check(config: &RuntimeConfig) -> Result<EvaluatedCheck, RuntimeError> {
+fn account_check(
+ config: &RuntimeConfig,
+ account_resolution: &crate::runtime::accounts::AccountResolution,
+) -> Result<EvaluatedCheck, RuntimeError> {
let snapshot = crate::runtime::accounts::snapshot(config)?;
if snapshot.accounts.is_empty() {
return Ok(EvaluatedCheck {
@@ -129,12 +134,36 @@ fn account_check(config: &RuntimeConfig) -> Result<EvaluatedCheck, RuntimeError>
});
}
- match crate::runtime::accounts::resolve_account(config)? {
+ match account_resolution.resolved_account.as_ref() {
Some(account) => {
- let detail = if account.selected {
- format!("{} selected", account.record.account_id)
- } else {
- format!("{} resolved by selector", account.record.account_id)
+ let detail = match account_resolution.source {
+ crate::runtime::accounts::AccountResolutionSource::InvocationOverride => {
+ match account_resolution.default_account.as_ref() {
+ Some(default) if default.record.account_id != account.record.account_id => {
+ format!(
+ "resolved account {} via invocation override; default account {} remains stored",
+ account.record.account_id, default.record.account_id
+ )
+ }
+ Some(default) => format!(
+ "resolved account {} via invocation override; default account {} is also stored",
+ account.record.account_id, default.record.account_id
+ ),
+ None => format!(
+ "resolved account {} via invocation override; no default account is stored",
+ account.record.account_id
+ ),
+ }
+ }
+ crate::runtime::accounts::AccountResolutionSource::DefaultAccount => {
+ format!(
+ "resolved account {} via default account",
+ account.record.account_id
+ )
+ }
+ crate::runtime::accounts::AccountResolutionSource::None => {
+ format!("resolved account {}", account.record.account_id)
+ }
};
Ok(EvaluatedCheck {
severity: DoctorSeverity::Ok,
@@ -151,10 +180,7 @@ fn account_check(config: &RuntimeConfig) -> Result<EvaluatedCheck, RuntimeError>
view: DoctorCheckView {
name: "account".to_owned(),
status: "warn".to_owned(),
- detail: format!(
- "accounts exist but no local account is selected in {}",
- config.account.store_path.display()
- ),
+ detail: crate::runtime::accounts::unresolved_account_reason(config)?,
},
action: Some("radroots account ls"),
}),
diff --git a/src/commands/identity.rs b/src/commands/identity.rs
@@ -4,8 +4,9 @@ use crate::domain::runtime::{
};
use crate::runtime::RuntimeError;
use crate::runtime::accounts::{
- AccountCreateMode, AccountRecordView, SHARED_ACCOUNT_STORE_SOURCE,
- create_or_migrate_selected_account, resolve_account, select_account, snapshot,
+ AccountCreateMode, AccountRecordView, SHARED_ACCOUNT_STORE_SOURCE, account_resolution_view,
+ account_summary_view, create_or_migrate_selected_account, resolve_account_resolution,
+ select_account, snapshot, unresolved_account_reason,
};
use crate::runtime::config::RuntimeConfig;
@@ -33,24 +34,22 @@ pub fn init(config: &RuntimeConfig) -> Result<AccountNewView, RuntimeError> {
}
pub fn show(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> {
- let view = match resolve_account(config)? {
+ let resolution = resolve_account_resolution(config)?;
+ let view = match resolution.resolved_account.as_ref() {
Some(account) => AccountWhoamiView {
state: "ready".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
reason: None,
+ account_resolution: account_resolution_view(&resolution),
public_identity: Some(IdentityPublicView::from_public_identity(
&account.record.public_identity,
)),
- account: Some(account_summary(&account)),
},
None => AccountWhoamiView {
state: "unconfigured".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- reason: Some(format!(
- "no local account is selected in {}",
- config.account.store_path.display()
- )),
- account: None,
+ reason: Some(unresolved_account_reason(config)?),
+ account_resolution: account_resolution_view(&resolution),
public_identity: None,
},
};
@@ -97,13 +96,13 @@ pub fn list(config: &RuntimeConfig) -> Result<CommandOutput, RuntimeError> {
pub fn use_account(config: &RuntimeConfig, selector: &str) -> Result<AccountUseView, RuntimeError> {
let account = select_account(config, selector)?;
Ok(AccountUseView {
- state: "active".to_owned(),
+ state: "default".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- active_account_id: account.record.account_id.to_string(),
+ default_account_id: account.record.account_id.to_string(),
account: account_summary(&account),
})
}
fn account_summary(account: &AccountRecordView) -> AccountSummaryView {
- AccountSummaryView::from_account_record(&account.record, account.signer, account.selected)
+ account_summary_view(account)
}
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -506,6 +506,7 @@ pub struct CapabilityBindingRuntimeView {
pub struct DoctorView {
pub ok: bool,
pub state: String,
+ pub account_resolution: AccountResolutionView,
pub checks: Vec<DoctorCheckView>,
pub source: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -561,13 +562,21 @@ impl AccountSummaryView {
}
#[derive(Debug, Clone, Serialize)]
+pub struct AccountResolutionView {
+ pub source: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub resolved_account: Option<AccountSummaryView>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default_account: Option<AccountSummaryView>,
+}
+
+#[derive(Debug, Clone, Serialize)]
pub struct AccountWhoamiView {
pub state: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub account: Option<AccountSummaryView>,
+ pub account_resolution: AccountResolutionView,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_identity: Option<IdentityPublicView>,
}
@@ -595,7 +604,7 @@ pub struct AccountNewView {
pub struct AccountUseView {
pub state: String,
pub source: String,
- pub active_account_id: String,
+ pub default_account_id: String,
pub account: AccountSummaryView,
}
@@ -693,8 +702,7 @@ impl SetupView {
pub struct StatusView {
pub state: String,
pub source: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub selected_account_id: Option<String>,
+ pub account_resolution: AccountResolutionView,
pub local_state: String,
pub local_root: String,
pub relay_state: String,
@@ -1912,8 +1920,7 @@ pub struct NetStatusView {
pub relay_count: usize,
pub publish_policy: String,
pub signer_mode: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub active_account_id: Option<String>,
+ pub account_resolution: AccountResolutionView,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -2093,7 +2100,8 @@ pub struct SignerStatusView {
pub state: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
- pub account_id: Option<String>,
+ pub signer_account_id: Option<String>,
+ pub account_resolution: AccountResolutionView,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub binding: SignerBindingStatusView,
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -226,10 +226,12 @@ fn render_human_view_to(
("mode", view.mode.as_str()),
("status", view.state.as_str()),
];
- if let Some(account_id) = &view.account_id {
- signer_rows.push(("account id", account_id.as_str()));
+ if let Some(account_id) = &view.signer_account_id {
+ signer_rows.push(("signer account id", account_id.as_str()));
}
render_pairs(stdout, "signer", signer_rows.as_slice())?;
+ writeln!(stdout)?;
+ render_account_resolution(stdout, &view.account_resolution)?;
if let Some(reason) = &view.reason {
writeln!(stdout, "reason: {reason}")?;
}
@@ -842,7 +844,7 @@ fn render_account_list(stdout: &mut dyn Write, view: &AccountListView) -> Result
("Account", account.id.clone()),
("Signer", humanize_machine_label(account.signer.as_str())),
(
- "Selected",
+ "Default",
if account.is_default {
"Yes".to_owned()
} else {
@@ -892,7 +894,7 @@ fn render_account_use(
stdout: &mut dyn Write,
view: &crate::domain::runtime::AccountUseView,
) -> Result<(), RuntimeError> {
- writeln!(stdout, "Account selected")?;
+ writeln!(stdout, "Default account updated")?;
writeln!(stdout)?;
render_account_section(stdout, &view.account)
}
@@ -903,11 +905,13 @@ fn render_account_whoami(
) -> Result<(), RuntimeError> {
match view.state.as_str() {
"ready" => {
- writeln!(stdout, "Selected account")?;
+ writeln!(stdout, "Resolved account")?;
writeln!(stdout)?;
- if let Some(account) = &view.account {
+ if let Some(account) = &view.account_resolution.resolved_account {
render_account_section(stdout, account)?;
}
+ writeln!(stdout)?;
+ render_account_resolution(stdout, &view.account_resolution)?;
if let Some(identity) = &view.public_identity {
writeln!(stdout)?;
writeln!(stdout, "Identity")?;
@@ -921,7 +925,9 @@ fn render_account_whoami(
writeln!(stdout, "{reason}")?;
}
writeln!(stdout)?;
- render_item_section(stdout, "Missing", &["Selected account".to_owned()])?;
+ render_account_resolution(stdout, &view.account_resolution)?;
+ writeln!(stdout)?;
+ render_item_section(stdout, "Missing", &["Resolved account".to_owned()])?;
writeln!(stdout)?;
render_item_section(stdout, "Next", &["radroots account create".to_owned()])?;
}
@@ -938,6 +944,29 @@ fn render_account_section(
push_row(&mut rows, "Name", account.display_name.clone());
rows.push(("Account", account.id.clone()));
rows.push(("Signer", humanize_machine_label(account.signer.as_str())));
+ rows.push((
+ "Default",
+ if account.is_default {
+ "Yes".to_owned()
+ } else {
+ "No".to_owned()
+ },
+ ));
+ render_field_rows(stdout, rows.as_slice())
+}
+
+fn render_account_resolution(
+ stdout: &mut dyn Write,
+ resolution: &crate::domain::runtime::AccountResolutionView,
+) -> Result<(), RuntimeError> {
+ writeln!(stdout, "Account resolution")?;
+ let mut rows = vec![("Source", humanize_machine_label(resolution.source.as_str()))];
+ if let Some(account) = &resolution.resolved_account {
+ rows.push(("Resolved account", account.id.clone()));
+ }
+ if let Some(account) = &resolution.default_account {
+ rows.push(("Default account", account.id.clone()));
+ }
render_field_rows(stdout, rows.as_slice())
}
@@ -1483,6 +1512,8 @@ fn render_doctor(stdout: &mut dyn Write, view: &DoctorView) -> Result<(), Runtim
}
render_item_section(stdout, "Next", &view.actions)?;
}
+ writeln!(stdout)?;
+ render_account_resolution(stdout, &view.account_resolution)?;
Ok(())
}
@@ -2907,17 +2938,16 @@ fn render_net_status(stdout: &mut dyn Write, view: &NetStatusView) -> Result<(),
},
)?;
let relay_count = view.relay_count.to_string();
- let mut rows = vec![
+ let rows = vec![
("status", view.state.as_str()),
("session", view.session.as_str()),
("relays configured", relay_count.as_str()),
("publish policy", view.publish_policy.as_str()),
("signer mode", view.signer_mode.as_str()),
];
- if let Some(account_id) = &view.active_account_id {
- rows.push(("active account id", account_id.as_str()));
- }
render_pairs(stdout, "network", rows.as_slice())?;
+ writeln!(stdout)?;
+ render_account_resolution(stdout, &view.account_resolution)?;
if let Some(reason) = &view.reason {
writeln!(stdout, "reason: {reason}")?;
}
@@ -3202,7 +3232,7 @@ fn render_farm_setup(stdout: &mut dyn Write, view: &FarmSetupView) -> Result<(),
"unconfigured" => {
writeln!(stdout, "Not ready yet")?;
writeln!(stdout)?;
- render_item_section(stdout, "Missing", &["Selected account".to_owned()])?;
+ render_item_section(stdout, "Missing", &["Resolved account".to_owned()])?;
if !view.actions.is_empty() {
writeln!(stdout)?;
render_item_section(stdout, "Next", &view.actions)?;
@@ -3580,7 +3610,9 @@ fn render_status_summary(stdout: &mut dyn Write, view: &StatusView) -> Result<()
&view.ready,
&view.needs_attention,
&view.next,
- )
+ )?;
+ writeln!(stdout)?;
+ render_account_resolution(stdout, &view.account_resolution)
}
fn render_checklist_summary(
@@ -4468,6 +4500,11 @@ mod tests {
let output = CommandOutput::unconfigured(CommandView::Doctor(DoctorView {
ok: false,
state: "warn".to_owned(),
+ account_resolution: crate::domain::runtime::AccountResolutionView {
+ source: "none".to_owned(),
+ resolved_account: None,
+ default_account: None,
+ },
checks: vec![
DoctorCheckView {
name: "config".to_owned(),
@@ -4501,6 +4538,11 @@ mod tests {
let output = CommandOutput::success(CommandView::Doctor(DoctorView {
ok: true,
state: "ok".to_owned(),
+ account_resolution: crate::domain::runtime::AccountResolutionView {
+ source: "default_account".to_owned(),
+ resolved_account: None,
+ default_account: None,
+ },
checks: vec![DoctorCheckView {
name: "config".to_owned(),
status: "ok".to_owned(),
diff --git a/src/runtime/accounts.rs b/src/runtime/accounts.rs
@@ -7,6 +7,7 @@ use radroots_secret_vault::{
RadrootsSecretVaultError, RadrootsSecretVaultOsKeyring,
};
+use crate::domain::runtime::{AccountResolutionView, AccountSummaryView};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
@@ -23,7 +24,7 @@ pub struct AccountSnapshot {
#[derive(Debug, Clone)]
pub struct AccountRecordView {
pub record: RadrootsNostrAccountRecord,
- pub selected: bool,
+ pub is_default: bool,
pub signer: &'static str,
}
@@ -49,6 +50,30 @@ pub struct AccountCreateResult {
pub account: AccountRecordView,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AccountResolutionSource {
+ InvocationOverride,
+ DefaultAccount,
+ None,
+}
+
+impl AccountResolutionSource {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::InvocationOverride => "invocation_override",
+ Self::DefaultAccount => "default_account",
+ Self::None => "none",
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct AccountResolution {
+ pub source: AccountResolutionSource,
+ pub resolved_account: Option<AccountRecordView>,
+ pub default_account: Option<AccountRecordView>,
+}
+
pub fn create_or_migrate_selected_account(
config: &RuntimeConfig,
) -> Result<AccountCreateResult, RuntimeError> {
@@ -66,11 +91,11 @@ pub fn create_or_migrate_selected_account(
let Some(account) = snapshot
.accounts
.into_iter()
- .find(|account| account.selected)
+ .find(|account| account.is_default)
else {
return Err(RuntimeError::Accounts(
radroots_nostr_accounts::prelude::RadrootsNostrAccountsError::InvalidState(
- "selected account missing after account create".to_owned(),
+ "default account missing after account create".to_owned(),
),
));
};
@@ -84,20 +109,40 @@ pub fn snapshot(config: &RuntimeConfig) -> Result<AccountSnapshot, RuntimeError>
}
pub fn resolve_account(config: &RuntimeConfig) -> Result<Option<AccountRecordView>, RuntimeError> {
+ Ok(resolve_account_resolution(config)?.resolved_account)
+}
+
+pub fn resolve_account_resolution(
+ config: &RuntimeConfig,
+) -> Result<AccountResolution, RuntimeError> {
let snapshot = snapshot(config)?;
+ let default_account = snapshot
+ .accounts
+ .iter()
+ .find(|account| account.is_default)
+ .cloned();
if let Some(selector) = config.account.selector.as_deref() {
let Some(account) = find_by_selector(snapshot.accounts.as_slice(), selector) else {
return Err(RuntimeError::Config(format!(
"account selector `{selector}` did not match any local account"
)));
};
- return Ok(Some(account.clone()));
+ return Ok(AccountResolution {
+ source: AccountResolutionSource::InvocationOverride,
+ resolved_account: Some(account.clone()),
+ default_account,
+ });
}
- Ok(snapshot
- .accounts
- .into_iter()
- .find(|account| account.selected))
+ Ok(AccountResolution {
+ source: if default_account.is_some() {
+ AccountResolutionSource::DefaultAccount
+ } else {
+ AccountResolutionSource::None
+ },
+ resolved_account: default_account.clone(),
+ default_account,
+ })
}
pub fn select_account(
@@ -121,37 +166,72 @@ pub fn select_account(
.ok_or_else(|| {
RuntimeError::Accounts(
radroots_nostr_accounts::prelude::RadrootsNostrAccountsError::InvalidState(
- "selected account missing after account use".to_owned(),
+ "default account missing after account use".to_owned(),
),
)
})
}
-pub fn selected_account_status(
+pub fn resolved_account_signing_status(
config: &RuntimeConfig,
) -> Result<RadrootsNostrSelectedAccountStatus, RuntimeError> {
let manager = account_manager(config)?;
- if let Some(selector) = config.account.selector.as_deref() {
- let snapshot = snapshot_from_manager(&manager)?;
- let Some(account) = find_by_selector(snapshot.accounts.as_slice(), selector) else {
- return Err(RuntimeError::Config(format!(
- "account selector `{selector}` did not match any local account"
- )));
- };
+ let resolution = resolve_account_resolution(config)?;
+ let Some(account) = resolution.resolved_account else {
+ return Ok(RadrootsNostrSelectedAccountStatus::NotConfigured);
+ };
- return Ok(
- match manager.get_signing_identity(&account.record.account_id)? {
- Some(_) => RadrootsNostrSelectedAccountStatus::Ready {
- account: account.record.clone(),
- },
- None => RadrootsNostrSelectedAccountStatus::PublicOnly {
- account: account.record.clone(),
- },
+ Ok(
+ match manager.get_signing_identity(&account.record.account_id)? {
+ Some(_) => RadrootsNostrSelectedAccountStatus::Ready {
+ account: account.record.clone(),
+ },
+ None => RadrootsNostrSelectedAccountStatus::PublicOnly {
+ account: account.record.clone(),
},
- );
+ },
+ )
+}
+
+pub fn account_summary_view(account: &AccountRecordView) -> AccountSummaryView {
+ AccountSummaryView::from_account_record(&account.record, account.signer, account.is_default)
+}
+
+pub fn account_resolution_view(resolution: &AccountResolution) -> AccountResolutionView {
+ AccountResolutionView {
+ source: resolution.source.as_str().to_owned(),
+ resolved_account: resolution
+ .resolved_account
+ .as_ref()
+ .map(account_summary_view),
+ default_account: resolution
+ .default_account
+ .as_ref()
+ .map(account_summary_view),
+ }
+}
+
+pub fn empty_account_resolution_view() -> AccountResolutionView {
+ AccountResolutionView {
+ source: AccountResolutionSource::None.as_str().to_owned(),
+ resolved_account: None,
+ default_account: None,
}
+}
- Ok(manager.selected_account_status()?)
+pub fn unresolved_account_reason(config: &RuntimeConfig) -> Result<String, RuntimeError> {
+ let snapshot = snapshot(config)?;
+ Ok(if snapshot.accounts.is_empty() {
+ format!(
+ "no local accounts found in {}",
+ config.account.store_path.display()
+ )
+ } else {
+ format!(
+ "accounts exist in {} but no default account is configured and no invocation override was provided",
+ config.account.store_path.display()
+ )
+ })
}
pub fn secret_backend_status(config: &RuntimeConfig) -> AccountSecretBackendStatus {
@@ -197,7 +277,7 @@ fn snapshot_from_manager(
.list_accounts()?
.into_iter()
.map(|record| AccountRecordView {
- selected: selected_account_id
+ is_default: selected_account_id
.as_deref()
.is_some_and(|selected| selected == record.account_id.as_str()),
signer: "local",
@@ -314,7 +394,6 @@ enum SecretBackendResolutionError {
#[cfg(test)]
mod tests {
- use super::*;
use radroots_protected_store::RadrootsProtectedFileSecretVault;
use radroots_secret_vault::RadrootsSecretVault;
use std::fs;
diff --git a/src/runtime/network.rs b/src/runtime/network.rs
@@ -36,8 +36,7 @@ pub fn relay_list(config: &RuntimeConfig) -> RelayListView {
}
pub fn net_status(config: &RuntimeConfig) -> Result<NetStatusView, RuntimeError> {
- let active_account_id =
- accounts::resolve_account(config)?.map(|account| account.record.account_id.to_string());
+ let account_resolution = accounts::resolve_account_resolution(config)?;
let relay_count = config.relay.urls.len();
let configured = relay_count > 0;
@@ -56,7 +55,7 @@ pub fn net_status(config: &RuntimeConfig) -> Result<NetStatusView, RuntimeError>
relay_count,
publish_policy: config.relay.publish_policy.as_str().to_owned(),
signer_mode: config.signer.backend.as_str().to_owned(),
- active_account_id,
+ account_resolution: accounts::account_resolution_view(&account_resolution),
reason: (!configured)
.then_some("no relays are configured for this operator session".to_owned()),
actions: relay_actions(config),
diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs
@@ -2,7 +2,7 @@ use crate::domain::runtime::{
IdentityPublicView, LocalSignerStatusView, MycRemoteSessionView, MycStatusView,
SignerBindingStatusView, SignerStatusView,
};
-use crate::runtime::accounts::SHARED_ACCOUNT_STORE_SOURCE;
+use crate::runtime::accounts::{SHARED_ACCOUNT_STORE_SOURCE, empty_account_resolution_view};
use crate::runtime::config::{
CapabilityBindingConfig, CapabilityBindingTargetKind, RuntimeConfig,
SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend,
@@ -100,13 +100,37 @@ pub fn resolve_actor_write_authority(
}
fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
+ let (account_resolution, resolved_account_id) =
+ match crate::runtime::accounts::resolve_account_resolution(config) {
+ Ok(resolution) => (
+ crate::runtime::accounts::account_resolution_view(&resolution),
+ resolution
+ .resolved_account
+ .as_ref()
+ .map(|account| account.record.account_id.to_string()),
+ ),
+ Err(error) => {
+ return SignerStatusView {
+ mode: config.signer.backend.as_str().to_owned(),
+ state: "error".to_owned(),
+ source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
+ signer_account_id: None,
+ account_resolution: empty_account_resolution_view(),
+ reason: Some(error.to_string()),
+ binding: disabled_binding_status(),
+ local: None,
+ myc: None,
+ };
+ }
+ };
let secret_backend = crate::runtime::accounts::secret_backend_status(config);
if secret_backend.state == "unavailable" {
return SignerStatusView {
mode: config.signer.backend.as_str().to_owned(),
state: "unavailable".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- account_id: None,
+ signer_account_id: resolved_account_id.clone(),
+ account_resolution: account_resolution.clone(),
reason: secret_backend.reason,
binding: disabled_binding_status(),
local: None,
@@ -119,7 +143,8 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
mode: config.signer.backend.as_str().to_owned(),
state: "error".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- account_id: None,
+ signer_account_id: resolved_account_id.clone(),
+ account_resolution: account_resolution.clone(),
reason: secret_backend.reason,
binding: disabled_binding_status(),
local: None,
@@ -132,7 +157,7 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
.unwrap_or_else(|| "unknown".to_owned());
let used_fallback = secret_backend.used_fallback;
- match crate::runtime::accounts::selected_account_status(config) {
+ match crate::runtime::accounts::resolved_account_signing_status(config) {
Ok(RadrootsNostrSelectedAccountStatus::Ready { account }) => {
let capability = RadrootsNostrSignerCapability::LocalAccount(
RadrootsNostrLocalSignerCapability::new(
@@ -149,7 +174,8 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
mode: config.signer.backend.as_str().to_owned(),
state: "ready".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- account_id: Some(local.account_id.to_string()),
+ signer_account_id: Some(local.account_id.to_string()),
+ account_resolution: account_resolution.clone(),
reason: None,
binding: disabled_binding_status(),
local: Some(LocalSignerStatusView {
@@ -169,7 +195,8 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
mode: config.signer.backend.as_str().to_owned(),
state: "unconfigured".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- account_id: Some(account.account_id.to_string()),
+ signer_account_id: Some(account.account_id.to_string()),
+ account_resolution: account_resolution.clone(),
reason: Some(format!(
"local account {} is present but not secret-backed",
account.account_id
@@ -190,11 +217,9 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
mode: config.signer.backend.as_str().to_owned(),
state: "unconfigured".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- account_id: None,
- reason: Some(format!(
- "no local account is selected in {}",
- config.account.store_path.display()
- )),
+ signer_account_id: None,
+ account_resolution: account_resolution.clone(),
+ reason: crate::runtime::accounts::unresolved_account_reason(config).ok(),
binding: disabled_binding_status(),
local: None,
myc: None,
@@ -203,7 +228,8 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
mode: config.signer.backend.as_str().to_owned(),
state: "error".to_owned(),
source: SHARED_ACCOUNT_STORE_SOURCE.to_owned(),
- account_id: None,
+ signer_account_id: resolved_account_id,
+ account_resolution,
reason: Some(error.to_string()),
binding: disabled_binding_status(),
local: None,
@@ -213,6 +239,10 @@ fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
}
fn resolve_myc_signer_status(config: &RuntimeConfig) -> SignerStatusView {
+ let account_resolution = match crate::runtime::accounts::resolve_account_resolution(config) {
+ Ok(resolution) => crate::runtime::accounts::account_resolution_view(&resolution),
+ Err(_) => empty_account_resolution_view(),
+ };
let myc = crate::runtime::myc::resolve_status(&config.myc);
let resolution = resolve_myc_binding(config, &myc);
let binding = resolution.view;
@@ -225,7 +255,8 @@ fn resolve_myc_signer_status(config: &RuntimeConfig) -> SignerStatusView {
} else {
myc.source.clone()
},
- account_id: resolution.resolved_account_id,
+ signer_account_id: resolution.resolved_account_id,
+ account_resolution,
reason: if myc.state == "ready" {
binding.reason.clone()
} else {
diff --git a/src/runtime/workflow.rs b/src/runtime/workflow.rs
@@ -66,7 +66,7 @@ pub fn setup(config: &RuntimeConfig, role: SetupRoleArg) -> Result<SetupView, Ru
}
pub fn status(config: &RuntimeConfig) -> Result<StatusView, RuntimeError> {
- let account = accounts::resolve_account(config)?;
+ let account_resolution = accounts::resolve_account_resolution(config)?;
let local_status = local::status(config)?;
let farm = inspect_farm(config)?;
let relay_configured = relay_configured(config);
@@ -77,11 +77,11 @@ pub fn status(config: &RuntimeConfig) -> Result<StatusView, RuntimeError> {
let mut next = Vec::new();
let mut state = "ready";
- if account.is_some() {
- ready.push("Selected account".to_owned());
+ if account_resolution.resolved_account.is_some() {
+ ready.push("Resolved account".to_owned());
} else {
state = "unconfigured";
- needs_attention.push("Selected account".to_owned());
+ needs_attention.push("Resolved account".to_owned());
}
if local_status.state == "ready" {
@@ -111,7 +111,10 @@ pub fn status(config: &RuntimeConfig) -> Result<StatusView, RuntimeError> {
} else {
push_next(&mut next, Some("radroots setup buyer"));
push_next(&mut next, Some("radroots setup seller"));
- if account.is_some() && local_status.state == "ready" && !relay_configured {
+ if account_resolution.resolved_account.is_some()
+ && local_status.state == "ready"
+ && !relay_configured
+ {
next.clear();
push_next(&mut next, Some(RELAY_SETUP_ACTION));
push_next(&mut next, Some("radroots status"));
@@ -121,7 +124,7 @@ pub fn status(config: &RuntimeConfig) -> Result<StatusView, RuntimeError> {
Ok(StatusView {
state: state.to_owned(),
source: WORKFLOW_SOURCE.to_owned(),
- selected_account_id: account.map(|account| account.record.account_id.to_string()),
+ account_resolution: accounts::account_resolution_view(&account_resolution),
local_state: local_status.state,
local_root: local_status.local_root,
relay_state: relay_state(config).to_owned(),
diff --git a/tests/doctor.rs b/tests/doctor.rs
@@ -55,6 +55,7 @@ fn doctor_reports_unconfigured_local_bootstrap_state() {
assert_eq!(json["checks"][0]["status"], "ok");
assert_eq!(json["checks"][1]["name"], "account");
assert_eq!(json["checks"][1]["status"], "warn");
+ assert_eq!(json["account_resolution"]["source"], "none");
assert_eq!(json["checks"][2]["name"], "relays");
assert_eq!(json["checks"][2]["status"], "warn");
assert_eq!(json["checks"][3]["name"], "signer");
@@ -87,6 +88,7 @@ fn doctor_reports_warn_for_ready_local_bootstrap_without_workflow_provider() {
assert_eq!(json["state"], "warn");
assert_eq!(json["checks"][1]["name"], "account");
assert_eq!(json["checks"][1]["status"], "ok");
+ assert_eq!(json["account_resolution"]["source"], "default_account");
assert_eq!(json["checks"][2]["name"], "relays");
assert_eq!(json["checks"][2]["status"], "ok");
assert_eq!(json["checks"][3]["name"], "signer");
diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs
@@ -182,9 +182,20 @@ fn account_whoami_json_reads_selected_account() {
let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
assert_eq!(json["state"], "ready");
assert_eq!(json["source"], "shared account store · local first");
- assert!(json["account"]["id"].is_string());
- assert_eq!(json["account"]["signer"], "local");
- assert_eq!(json["account"]["is_default"], true);
+ assert_eq!(json["account_resolution"]["source"], "default_account");
+ assert!(json["account_resolution"]["resolved_account"]["id"].is_string());
+ assert_eq!(
+ json["account_resolution"]["resolved_account"]["signer"],
+ "local"
+ );
+ assert_eq!(
+ json["account_resolution"]["resolved_account"]["is_default"],
+ true
+ );
+ assert_eq!(
+ json["account_resolution"]["default_account"]["id"],
+ json["account_resolution"]["resolved_account"]["id"]
+ );
assert!(json["public_identity"]["id"].is_string());
}
@@ -202,12 +213,14 @@ fn account_whoami_json_reports_unconfigured_without_accounts() {
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
let json: Value = serde_json::from_str(stdout.as_str()).expect("json output");
assert_eq!(json["state"], "unconfigured");
- assert_eq!(json["account"], Value::Null);
+ assert_eq!(json["account_resolution"]["source"], "none");
+ assert_eq!(json["account_resolution"]["resolved_account"], Value::Null);
+ assert_eq!(json["account_resolution"]["default_account"], Value::Null);
assert_eq!(json["public_identity"], Value::Null);
assert!(
json["reason"]
.as_str()
- .is_some_and(|value| value.contains("no local account is selected"))
+ .is_some_and(|value| value.contains("no local accounts found"))
);
}
@@ -270,8 +283,8 @@ fn account_use_selects_existing_account() {
assert!(use_output.status.success());
let use_json: Value =
serde_json::from_slice(use_output.stdout.as_slice()).expect("account use json");
- assert_eq!(use_json["state"], "active");
- assert_eq!(use_json["active_account_id"], first_id);
+ assert_eq!(use_json["state"], "default");
+ assert_eq!(use_json["default_account_id"], first_id);
assert_eq!(use_json["account"]["is_default"], true);
let whoami = cli_command_in(dir.path())
@@ -281,6 +294,16 @@ fn account_use_selects_existing_account() {
assert!(whoami.status.success());
let whoami_json: Value =
serde_json::from_slice(whoami.stdout.as_slice()).expect("account whoami json");
- assert_eq!(whoami_json["account"]["id"], first_id);
- assert_eq!(whoami_json["account"]["is_default"], true);
+ assert_eq!(
+ whoami_json["account_resolution"]["source"],
+ "default_account"
+ );
+ assert_eq!(
+ whoami_json["account_resolution"]["resolved_account"]["id"],
+ first_id
+ );
+ assert_eq!(
+ whoami_json["account_resolution"]["resolved_account"]["is_default"],
+ true
+ );
}
diff --git a/tests/myc_status.rs b/tests/myc_status.rs
@@ -175,7 +175,7 @@ fn signer_status_reports_degraded_myc_backend_as_external_unavailable() {
assert_eq!(json["mode"], "myc");
assert_eq!(json["state"], "degraded");
assert_eq!(json["source"], "myc status command · local first");
- assert_eq!(json["account_id"], Value::Null);
+ assert_eq!(json["signer_account_id"], Value::Null);
assert_eq!(json["myc"]["state"], "degraded");
assert_eq!(json["myc"]["service_status"], "degraded");
assert_eq!(json["binding"]["state"], "unconfigured");
@@ -235,7 +235,7 @@ signer_session_ref = "{signer_session_ref}"
assert_eq!(json["mode"], "myc");
assert_eq!(json["state"], "ready");
assert_eq!(json["source"], "workspace config [[capability_binding]]");
- assert_eq!(json["account_id"], managed_account_ref);
+ assert_eq!(json["signer_account_id"], managed_account_ref);
assert_eq!(json["binding"]["state"], "ready");
assert_eq!(
json["binding"]["resolved_signer_session_id"],
diff --git a/tests/relay_net.rs b/tests/relay_net.rs
@@ -142,6 +142,10 @@ fn net_status_json_reports_effective_network_configuration() {
assert_eq!(json["relay_count"], 2);
assert_eq!(json["publish_policy"], "any");
assert_eq!(json["signer_mode"], "local");
- assert_eq!(json["active_account_id"], account_id);
+ assert_eq!(json["account_resolution"]["source"], "default_account");
+ assert_eq!(
+ json["account_resolution"]["resolved_account"]["id"],
+ account_id
+ );
assert_eq!(json["source"], "cli flags · local first");
}
diff --git a/tests/signer_status.rs b/tests/signer_status.rs
@@ -69,7 +69,8 @@ fn signer_status_reports_local_ready_when_account_exists() {
assert_eq!(json["mode"], "local");
assert_eq!(json["state"], "ready");
assert_eq!(json["source"], "shared account store · local first");
- assert_eq!(json["account_id"], json["local"]["account_id"]);
+ assert_eq!(json["signer_account_id"], json["local"]["account_id"]);
+ assert_eq!(json["account_resolution"]["source"], "default_account");
assert_eq!(json["reason"], Value::Null);
assert_eq!(json["binding"]["state"], "disabled");
assert_eq!(json["binding"]["source"], "independent local signer mode");
@@ -97,7 +98,7 @@ fn signer_status_reports_local_unconfigured_when_no_account_is_selected() {
assert!(
json["reason"]
.as_str()
- .is_some_and(|value| value.contains("no local account is selected"))
+ .is_some_and(|value| value.contains("no local accounts found"))
);
assert_eq!(json["local"], Value::Null);
}
@@ -161,7 +162,12 @@ fn signer_status_honors_explicit_account_selector_over_default_account() {
let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("signer json");
assert_eq!(json["mode"], "local");
assert_eq!(json["state"], "ready");
- assert_eq!(json["account_id"], first_id);
+ assert_eq!(json["signer_account_id"], first_id);
+ assert_eq!(json["account_resolution"]["source"], "invocation_override");
+ assert_eq!(
+ json["account_resolution"]["resolved_account"]["id"],
+ first_id
+ );
assert_eq!(json["local"]["account_id"], first_id);
assert_eq!(json["local"]["backend"], "encrypted_file");
assert_eq!(json["local"]["used_fallback"], true);
diff --git a/tests/workflow.rs b/tests/workflow.rs
@@ -102,7 +102,7 @@ fn status_is_unconfigured_before_setup() {
assert_eq!(
json["needs_attention"],
serde_json::json!([
- "Selected account",
+ "Resolved account",
"Local market data",
"Relay configuration"
])
@@ -132,7 +132,7 @@ fn status_calls_out_missing_relay_after_buyer_setup() {
assert_eq!(json["state"], "unconfigured");
assert_eq!(
json["ready"],
- serde_json::json!(["Selected account", "Local market data"])
+ serde_json::json!(["Resolved account", "Local market data"])
);
assert_eq!(
json["needs_attention"],
@@ -190,7 +190,8 @@ fn status_reports_farm_publish_need_when_core_state_is_ready() {
let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
assert!(stdout.contains("Status"));
assert!(stdout.contains("Ready"));
- assert!(stdout.contains("Selected account"));
+ assert!(stdout.contains("Resolved account"));
+ assert!(stdout.contains("Account resolution"));
assert!(stdout.contains("Local market data"));
assert!(stdout.contains("Relay configuration"));
assert!(stdout.contains("Needs attention"));