use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::keys::seed_store::SeedStore;
use crate::store::KeyspaceHandle;
const ACTIVE_SEED_ID_KEY: &str = "active_seed_id";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SeedRecord {
pub id: u32,
pub seed_hex: Option<String>,
pub created_at: DateTime<Utc>,
pub retired_at: Option<DateTime<Utc>>,
}
fn store_seed_key(id: u32) -> String {
format!("seed:{id}")
}
pub async fn get_active_seed_id(
keys_ks: &KeyspaceHandle,
) -> Result<u32, Box<dyn std::error::Error>> {
match keys_ks.get_raw(ACTIVE_SEED_ID_KEY).await? {
Some(bytes) => {
let arr: [u8; 4] = bytes
.try_into()
.map_err(|_| "active_seed_id is not 4 bytes")?;
Ok(u32::from_le_bytes(arr))
}
None => Ok(0),
}
}
pub async fn set_active_seed_id(
keys_ks: &KeyspaceHandle,
id: u32,
) -> Result<(), Box<dyn std::error::Error>> {
keys_ks
.insert_raw(ACTIVE_SEED_ID_KEY, id.to_le_bytes().to_vec())
.await?;
Ok(())
}
pub async fn get_seed_record(
keys_ks: &KeyspaceHandle,
id: u32,
) -> Result<Option<SeedRecord>, Box<dyn std::error::Error>> {
Ok(keys_ks.get(store_seed_key(id)).await?)
}
pub async fn save_seed_record(
keys_ks: &KeyspaceHandle,
record: &SeedRecord,
) -> Result<(), Box<dyn std::error::Error>> {
keys_ks.insert(store_seed_key(record.id), record).await?;
Ok(())
}
pub async fn list_seed_records(
keys_ks: &KeyspaceHandle,
) -> Result<Vec<SeedRecord>, Box<dyn std::error::Error>> {
let raw = keys_ks.prefix_iter_raw("seed:").await?;
let mut records = Vec::with_capacity(raw.len());
for (_key, value) in raw {
let record: SeedRecord = serde_json::from_slice(&value)?;
records.push(record);
}
records.sort_by_key(|r| r.id);
Ok(records)
}
pub async fn load_seed_bytes(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
seed_id: Option<u32>,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let effective_id = seed_id.unwrap_or(0);
if let Some(record) = get_seed_record(keys_ks, effective_id).await?
&& let Some(ref hex_str) = record.seed_hex
{
return Ok(hex::decode(hex_str)?);
}
seed_store
.get()
.await
.map_err(|e| format!("{e}"))?
.ok_or_else(|| "no seed found in external store".into())
}
pub async fn rotate_seed(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
mnemonic: Option<&str>,
) -> Result<u32, Box<dyn std::error::Error>> {
let old_id = get_active_seed_id(keys_ks).await?;
let old_seed = seed_store
.get()
.await
.map_err(|e| format!("{e}"))?
.ok_or("no active seed found — cannot rotate")?;
let mut old_record = get_seed_record(keys_ks, old_id)
.await?
.unwrap_or_else(|| SeedRecord {
id: old_id,
seed_hex: None,
created_at: Utc::now(),
retired_at: None,
});
old_record.seed_hex = Some(hex::encode(&old_seed));
old_record.retired_at = Some(Utc::now());
save_seed_record(keys_ks, &old_record).await?;
info!(seed_id = old_id, "archived retired seed");
let new_seed: Vec<u8> = if let Some(phrase) = mnemonic {
let m =
bip39::Mnemonic::parse(phrase).map_err(|e| format!("invalid BIP-39 mnemonic: {e}"))?;
m.to_seed("").to_vec()
} else {
let mut buf = [0u8; 32];
rand::Rng::fill_bytes(&mut rand::rng(), &mut buf);
buf.to_vec()
};
seed_store
.set(&new_seed)
.await
.map_err(|e| format!("{e}"))?;
let new_id = old_id + 1;
let new_record = SeedRecord {
id: new_id,
seed_hex: None,
created_at: Utc::now(),
retired_at: None,
};
save_seed_record(keys_ks, &new_record).await?;
set_active_seed_id(keys_ks, new_id).await?;
info!(
old_seed_id = old_id,
new_seed_id = new_id,
"seed rotated successfully"
);
Ok(new_id)
}