feat: BIP-39 master seed for unified key derivation
Replace fragmented random key generation with a single 24-word BIP-39 mnemonic that deterministically derives all node keys: Ed25519 (DID), secp256k1 (Nostr/Bitcoin), BIP-84 xprv (Bitcoin Core), and LND aezeed entropy. New onboarding flow: seed generate → word verification → identity naming. Restore path enabled via 24-word entry. Includes seed RPC handlers, mock backend support, LND/Bitcoin Core wallet-from-seed integration, and UI polish across settings and discover views. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5da9e217e6
commit
19dcfd4f31
@@ -1,6 +1,7 @@
|
||||
use super::RpcHandler;
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BitcoinInfo {
|
||||
@@ -106,4 +107,117 @@ impl RpcHandler {
|
||||
.result
|
||||
.ok_or_else(|| anyhow::anyhow!("Bitcoin RPC returned null result"))
|
||||
}
|
||||
|
||||
/// Initialize a Bitcoin Core descriptor wallet with keys derived from the master seed.
|
||||
/// Creates a blank wallet and imports BIP-84 (native segwit) descriptors.
|
||||
/// Requires: password re-verification, encrypted seed on disk.
|
||||
pub(super) async fn handle_bitcoin_init_wallet_from_seed(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let password = params.get("password")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'password' for seed access"))?;
|
||||
let wallet_name = params.get("wallet_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("archipelago");
|
||||
|
||||
// Verify user password.
|
||||
self.auth_manager.verify_password(password).await
|
||||
.context("Password verification failed")?;
|
||||
|
||||
// Load encrypted seed.
|
||||
let mnemonic = crate::seed::load_seed_encrypted(&self.config.data_dir, password).await
|
||||
.context("Failed to load encrypted seed")?;
|
||||
let seed = crate::seed::MasterSeed::from_mnemonic(&mnemonic);
|
||||
|
||||
// Derive BIP-84 account xprv.
|
||||
let xprv = crate::seed::derive_bitcoin_xprv(&seed)?;
|
||||
let mut xprv_str = xprv.to_string();
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
// Step 1: Create a blank descriptor wallet.
|
||||
let create_result = self.bitcoin_rpc_call::<serde_json::Value>(
|
||||
&client,
|
||||
"createwallet",
|
||||
&[
|
||||
serde_json::json!(wallet_name), // wallet_name
|
||||
serde_json::json!(false), // disable_private_keys
|
||||
serde_json::json!(true), // blank
|
||||
serde_json::json!(""), // passphrase
|
||||
serde_json::json!(false), // avoid_reuse
|
||||
serde_json::json!(true), // descriptors
|
||||
],
|
||||
).await;
|
||||
|
||||
match create_result {
|
||||
Ok(_) => tracing::info!("Created blank descriptor wallet '{}'", wallet_name),
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("already exists") {
|
||||
tracing::info!("Wallet '{}' already exists, importing descriptors", wallet_name);
|
||||
} else {
|
||||
xprv_str.zeroize();
|
||||
return Err(e.context("Failed to create wallet"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Import BIP-84 descriptors (external + internal/change).
|
||||
// Format: wpkh(xprv/0/*) for receive, wpkh(xprv/1/*) for change.
|
||||
let external_desc = format!("wpkh({}/0/*)", xprv_str);
|
||||
let internal_desc = format!("wpkh({}/1/*)", xprv_str);
|
||||
|
||||
// Get checksums from Bitcoin Core.
|
||||
let ext_info: serde_json::Value = self.bitcoin_rpc_call(
|
||||
&client, "getdescriptorinfo", &[serde_json::json!(external_desc)],
|
||||
).await.context("getdescriptorinfo failed for external descriptor")?;
|
||||
|
||||
let int_info: serde_json::Value = self.bitcoin_rpc_call(
|
||||
&client, "getdescriptorinfo", &[serde_json::json!(internal_desc)],
|
||||
).await.context("getdescriptorinfo failed for internal descriptor")?;
|
||||
|
||||
let ext_desc_with_checksum = ext_info.get("descriptor")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("No descriptor in getdescriptorinfo response"))?;
|
||||
let int_desc_with_checksum = int_info.get("descriptor")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("No descriptor in getdescriptorinfo response"))?;
|
||||
|
||||
let import_params = serde_json::json!([
|
||||
{
|
||||
"desc": ext_desc_with_checksum,
|
||||
"timestamp": "now",
|
||||
"active": true,
|
||||
"internal": false,
|
||||
"range": [0, 1000],
|
||||
},
|
||||
{
|
||||
"desc": int_desc_with_checksum,
|
||||
"timestamp": "now",
|
||||
"active": true,
|
||||
"internal": true,
|
||||
"range": [0, 1000],
|
||||
}
|
||||
]);
|
||||
|
||||
let _import_result: serde_json::Value = self.bitcoin_rpc_call(
|
||||
&client, "importdescriptors", &[import_params],
|
||||
).await.context("importdescriptors failed")?;
|
||||
|
||||
// Zeroize the xprv string from memory.
|
||||
xprv_str.zeroize();
|
||||
|
||||
tracing::info!("Bitcoin Core wallet '{}' initialized from master seed (BIP-84)", wallet_name);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"initialized": true,
|
||||
"wallet_name": wallet_name,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,13 @@ impl RpcHandler {
|
||||
"auth.isOnboardingComplete" => self.handle_auth_is_onboarding_complete().await,
|
||||
"auth.resetOnboarding" => self.handle_auth_reset_onboarding(params).await,
|
||||
|
||||
// Seed management (BIP-39 mnemonic)
|
||||
"seed.generate" => self.handle_seed_generate().await,
|
||||
"seed.verify" => self.handle_seed_verify(params).await,
|
||||
"seed.restore" => self.handle_seed_restore(params).await,
|
||||
"seed.save-encrypted" => self.handle_seed_save_encrypted(params).await,
|
||||
"seed.status" => self.handle_seed_status().await,
|
||||
|
||||
// Container orchestration (for Archipelago-managed containers)
|
||||
"container-install" => self.handle_container_install(params).await,
|
||||
"container-start" => self.handle_container_start(params).await,
|
||||
@@ -78,6 +85,7 @@ impl RpcHandler {
|
||||
|
||||
// Bitcoin & Lightning deep data
|
||||
"bitcoin.getinfo" => self.handle_bitcoin_getinfo().await,
|
||||
"bitcoin.init-wallet-from-seed" => self.handle_bitcoin_init_wallet_from_seed(params).await,
|
||||
"lnd.getinfo" => self.handle_lnd_getinfo().await,
|
||||
"lnd.listchannels" => self.handle_lnd_listchannels().await,
|
||||
"lnd.openchannel" => self.handle_lnd_openchannel(params).await,
|
||||
@@ -92,6 +100,7 @@ impl RpcHandler {
|
||||
"lnd.gettransactions" => self.handle_lnd_gettransactions().await,
|
||||
"lnd.connect-info" => self.handle_lnd_connect_info().await,
|
||||
"lnd.export-channel-backup" => self.handle_lnd_export_channel_backup().await,
|
||||
"lnd.init-wallet-from-seed" => self.handle_lnd_init_wallet_from_seed(params).await,
|
||||
|
||||
// Multi-identity management
|
||||
"identity.list" => self.handle_identity_list(params).await,
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::api::rpc::RpcHandler;
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine;
|
||||
use tracing::info;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
impl RpcHandler {
|
||||
/// Generate a new on-chain Bitcoin address.
|
||||
@@ -381,4 +382,71 @@ impl RpcHandler {
|
||||
"broadcast": false,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Initialize LND wallet with entropy derived from the node's BIP-39 master seed.
|
||||
/// The 16-byte entropy deterministically produces an aezeed mnemonic inside LND.
|
||||
/// Requires: password re-verification via params.password, encrypted seed on disk.
|
||||
pub(in crate::api::rpc) async fn handle_lnd_init_wallet_from_seed(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let password = params.get("password")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'password' for seed access"))?;
|
||||
let wallet_password = params.get("wallet_password")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'wallet_password' for LND"))?;
|
||||
|
||||
// Verify user password before granting seed access.
|
||||
self.auth_manager.verify_password(password).await
|
||||
.context("Password verification failed")?;
|
||||
|
||||
// Load encrypted seed from disk.
|
||||
let mnemonic = crate::seed::load_seed_encrypted(&self.config.data_dir, password).await
|
||||
.context("Failed to load encrypted seed. Was a seed phrase saved during onboarding?")?;
|
||||
let seed = crate::seed::MasterSeed::from_mnemonic(&mnemonic);
|
||||
|
||||
// Derive 16 bytes of LND entropy.
|
||||
let mut entropy = crate::seed::derive_lnd_entropy(&seed)?;
|
||||
let entropy_b64 = base64::engine::general_purpose::STANDARD.encode(&entropy);
|
||||
entropy.zeroize();
|
||||
|
||||
let wallet_password_b64 = base64::engine::general_purpose::STANDARD.encode(wallet_password.as_bytes());
|
||||
|
||||
// Call LND REST API to initialize wallet with derived entropy.
|
||||
// LND must be running but NOT yet initialized (no existing wallet).
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
let init_body = serde_json::json!({
|
||||
"wallet_password": wallet_password_b64,
|
||||
"seed_entropy": entropy_b64,
|
||||
});
|
||||
|
||||
let resp = client
|
||||
.post("https://127.0.0.1:8080/v1/initwallet")
|
||||
.json(&init_body)
|
||||
.send()
|
||||
.await
|
||||
.context("LND initwallet request failed — is LND running and uninitialized?")?;
|
||||
|
||||
let status = resp.status();
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.context("Failed to parse initwallet response")?;
|
||||
|
||||
if !status.is_success() {
|
||||
let msg = body.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error");
|
||||
return Err(anyhow::anyhow!("LND wallet init failed: {}", msg));
|
||||
}
|
||||
|
||||
info!("LND wallet initialized from master seed entropy");
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"initialized": true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ pub(super) const UNAUTHENTICATED_METHODS: &[&str] = &[
|
||||
"identity.create",
|
||||
"identity.verify",
|
||||
"identity.resolve-did",
|
||||
// Seed management (onboarding — before user has a session)
|
||||
"seed.generate",
|
||||
"seed.verify",
|
||||
"seed.restore",
|
||||
"seed.save-encrypted",
|
||||
// Onboarding restore (before user account exists)
|
||||
"backup.restore-identity",
|
||||
// Inter-node RPC: called by federated peers over Tor, no session cookies
|
||||
|
||||
@@ -24,6 +24,7 @@ mod package;
|
||||
mod peers;
|
||||
mod response;
|
||||
mod router;
|
||||
mod seed_rpc;
|
||||
mod security;
|
||||
mod tor;
|
||||
mod transport;
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! RPC handlers for BIP-39 seed management.
|
||||
//! Endpoints: seed.generate, seed.verify, seed.restore, seed.save-encrypted, seed.status
|
||||
|
||||
use super::RpcHandler;
|
||||
use anyhow::{Context, Result};
|
||||
use nostr_sdk::ToBech32;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// In-memory storage for the mnemonic between generate and verify steps.
|
||||
/// Auto-cleared after 10 minutes.
|
||||
static ONBOARDING_MNEMONIC: std::sync::LazyLock<Arc<Mutex<Option<OnboardingMnemonicState>>>> =
|
||||
std::sync::LazyLock::new(|| Arc::new(Mutex::new(None)));
|
||||
|
||||
struct OnboardingMnemonicState {
|
||||
words: String,
|
||||
created_at: std::time::Instant,
|
||||
}
|
||||
|
||||
impl Drop for OnboardingMnemonicState {
|
||||
fn drop(&mut self) {
|
||||
self.words.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
const MNEMONIC_TTL: std::time::Duration = std::time::Duration::from_secs(600); // 10 minutes
|
||||
|
||||
impl RpcHandler {
|
||||
/// Generate a new 24-word BIP-39 mnemonic, derive and persist node keys.
|
||||
/// Returns the words for the user to write down.
|
||||
pub(in crate::api::rpc) async fn handle_seed_generate(
|
||||
&self,
|
||||
) -> Result<serde_json::Value> {
|
||||
let (mnemonic, seed) = crate::seed::MasterSeed::generate()?;
|
||||
|
||||
// Derive and write node Ed25519 key.
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
crate::identity::NodeIdentity::from_seed(&identity_dir, &seed).await?;
|
||||
|
||||
// Derive and write node-level Nostr key.
|
||||
let nostr_keys = crate::seed::derive_node_nostr_key(&seed)?;
|
||||
let nostr_secret_path = identity_dir.join("nostr_secret");
|
||||
let nostr_pub_path = identity_dir.join("nostr_pubkey");
|
||||
let secret_hex = nostr_keys.secret_key().display_secret().to_string();
|
||||
let pubkey_hex = nostr_keys.public_key().to_hex();
|
||||
tokio::fs::write(&nostr_secret_path, secret_hex.as_bytes()).await?;
|
||||
tokio::fs::write(&nostr_pub_path, pubkey_hex.as_bytes()).await?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
tokio::fs::set_permissions(&nostr_secret_path, std::fs::Permissions::from_mode(0o600)).await?;
|
||||
}
|
||||
|
||||
// Initialize identity index at 0.
|
||||
crate::seed::save_identity_index(&self.config.data_dir, 0).await?;
|
||||
|
||||
let words: Vec<&str> = mnemonic.words().collect();
|
||||
|
||||
// Hold mnemonic in memory for the verify step.
|
||||
{
|
||||
let mut state = ONBOARDING_MNEMONIC.lock().await;
|
||||
*state = Some(OnboardingMnemonicState {
|
||||
words: mnemonic.to_string(),
|
||||
created_at: std::time::Instant::now(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"words": words,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Verify the user wrote down their seed correctly.
|
||||
/// Also confirms the mnemonic by re-deriving and returning DID + npub.
|
||||
pub(in crate::api::rpc) async fn handle_seed_verify(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let submitted_words: Vec<String> = serde_json::from_value(
|
||||
params.get("words").cloned().ok_or_else(|| anyhow::anyhow!("Missing words"))?,
|
||||
).context("Invalid words array")?;
|
||||
|
||||
// Validate against the held mnemonic.
|
||||
let mnemonic_str = {
|
||||
let mut state = ONBOARDING_MNEMONIC.lock().await;
|
||||
match state.as_ref() {
|
||||
Some(s) if s.created_at.elapsed() < MNEMONIC_TTL => s.words.clone(),
|
||||
_ => {
|
||||
*state = None;
|
||||
anyhow::bail!("No pending seed generation or session expired. Please regenerate.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let expected_words: Vec<&str> = mnemonic_str.split_whitespace().collect();
|
||||
let submitted: Vec<&str> = submitted_words.iter().map(|s| s.as_str()).collect();
|
||||
if expected_words != submitted {
|
||||
anyhow::bail!("Submitted words do not match generated seed");
|
||||
}
|
||||
|
||||
// Re-derive to get DID and npub.
|
||||
let (mnemonic, seed) = crate::seed::MasterSeed::from_mnemonic_words(&mnemonic_str)?;
|
||||
let node_key = crate::seed::derive_node_ed25519(&seed)?;
|
||||
let pubkey_hex = hex::encode(node_key.verifying_key().as_bytes());
|
||||
let did = crate::identity::did_key_from_pubkey_hex(&pubkey_hex)?;
|
||||
|
||||
let nostr_keys = crate::seed::derive_node_nostr_key(&seed)?;
|
||||
let nostr_npub = nostr_keys.public_key().to_bech32().unwrap_or_default();
|
||||
|
||||
// Clear mnemonic from memory now that it's verified.
|
||||
{
|
||||
let mut state = ONBOARDING_MNEMONIC.lock().await;
|
||||
*state = None;
|
||||
}
|
||||
|
||||
// Save the encrypted seed for convenience backup.
|
||||
// Use empty passphrase placeholder — the real encrypted save happens via seed.save-encrypted.
|
||||
// For now we just confirm the mnemonic was valid.
|
||||
drop(mnemonic);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"verified": true,
|
||||
"did": did,
|
||||
"nostr_npub": nostr_npub,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Restore node identity from a 24-word seed phrase.
|
||||
pub(in crate::api::rpc) async fn handle_seed_restore(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let words: Vec<String> = serde_json::from_value(
|
||||
params.get("words").cloned().ok_or_else(|| anyhow::anyhow!("Missing words"))?,
|
||||
).context("Invalid words array")?;
|
||||
|
||||
let phrase = words.join(" ");
|
||||
let (_mnemonic, seed) = crate::seed::MasterSeed::from_mnemonic_words(&phrase)?;
|
||||
|
||||
// Derive and write node Ed25519 key.
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
crate::identity::NodeIdentity::from_seed(&identity_dir, &seed).await?;
|
||||
|
||||
// Derive and write node-level Nostr key.
|
||||
let nostr_keys = crate::seed::derive_node_nostr_key(&seed)?;
|
||||
let secret_hex = nostr_keys.secret_key().display_secret().to_string();
|
||||
let pubkey_hex_nostr = nostr_keys.public_key().to_hex();
|
||||
tokio::fs::write(identity_dir.join("nostr_secret"), secret_hex.as_bytes()).await?;
|
||||
tokio::fs::write(identity_dir.join("nostr_pubkey"), pubkey_hex_nostr.as_bytes()).await?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
tokio::fs::set_permissions(
|
||||
identity_dir.join("nostr_secret"),
|
||||
std::fs::Permissions::from_mode(0o600),
|
||||
).await?;
|
||||
}
|
||||
|
||||
// Initialize identity index.
|
||||
crate::seed::save_identity_index(&self.config.data_dir, 0).await?;
|
||||
|
||||
// Create default identity from seed.
|
||||
let manager = crate::identity_manager::IdentityManager::new(&self.config.data_dir).await?;
|
||||
manager.create_from_seed(
|
||||
"Personal".to_string(),
|
||||
crate::identity_manager::IdentityPurpose::Personal,
|
||||
&seed,
|
||||
&self.config.data_dir,
|
||||
).await?;
|
||||
|
||||
// Get DID and npub for the response.
|
||||
let node_key = crate::seed::derive_node_ed25519(&seed)?;
|
||||
let pubkey_hex = hex::encode(node_key.verifying_key().as_bytes());
|
||||
let did = crate::identity::did_key_from_pubkey_hex(&pubkey_hex)?;
|
||||
let nostr_npub = nostr_keys.public_key().to_bech32().unwrap_or_default();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"did": did,
|
||||
"nostr_npub": nostr_npub,
|
||||
"restored": true,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Encrypt and save the mnemonic to disk for convenience backup.
|
||||
pub(in crate::api::rpc) async fn handle_seed_save_encrypted(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let passphrase = params.get("passphrase")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing passphrase"))?;
|
||||
|
||||
// Try to get mnemonic from in-memory state first.
|
||||
let mnemonic_str = {
|
||||
let state = ONBOARDING_MNEMONIC.lock().await;
|
||||
state.as_ref()
|
||||
.filter(|s| s.created_at.elapsed() < MNEMONIC_TTL)
|
||||
.map(|s| s.words.clone())
|
||||
};
|
||||
|
||||
let mnemonic: bip39::Mnemonic = if let Some(words) = mnemonic_str {
|
||||
words.parse().context("Invalid mnemonic in memory")?
|
||||
} else {
|
||||
anyhow::bail!("No mnemonic available. Generate or restore a seed first.");
|
||||
};
|
||||
|
||||
crate::seed::save_seed_encrypted(&self.config.data_dir, &mnemonic, passphrase).await?;
|
||||
|
||||
Ok(serde_json::json!({ "saved": true }))
|
||||
}
|
||||
|
||||
/// Return seed status information.
|
||||
pub(in crate::api::rpc) async fn handle_seed_status(
|
||||
&self,
|
||||
) -> Result<serde_json::Value> {
|
||||
let has_seed = crate::seed::seed_exists(&self.config.data_dir);
|
||||
let has_node_key = crate::identity::NodeIdentity::key_exists(
|
||||
&self.config.data_dir.join("identity"),
|
||||
);
|
||||
let is_legacy = has_node_key && !has_seed;
|
||||
let next_index = crate::seed::load_identity_index(&self.config.data_dir).await.unwrap_or(0);
|
||||
|
||||
let manager = crate::identity_manager::IdentityManager::new(&self.config.data_dir).await?;
|
||||
let (identities, _) = manager.list().await?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"has_seed": has_seed,
|
||||
"is_legacy": is_legacy,
|
||||
"identity_count": identities.len(),
|
||||
"next_index": next_index,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,7 @@ pub(super) fn parse_meminfo_kb(val: &str) -> Result<u64> {
|
||||
|
||||
/// Read disk usage via `df` for the root filesystem.
|
||||
/// Returns (used_bytes, total_bytes).
|
||||
#[allow(dead_code)]
|
||||
pub(super) async fn read_disk_usage() -> Result<(u64, u64)> {
|
||||
read_disk_usage_path("/").await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user