use std::sync::Arc;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tracing::warn;
use crate::error::WalletResult;
use crate::storage::find_args::{FindSettingsArgs, SettingsPartial};
use crate::storage::traits::wallet_provider::WalletStorageProvider;
const CACHE_TTL: Duration = Duration::from_secs(120);
const _SETTINGS_KEY: &str = "settings";
const _SETTINGS_BASKET: &str = "wallet settings";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrustedCertifier {
pub name: String,
pub description: String,
pub identity_key: String,
pub trust: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrustSettings {
pub trust_level: u32,
pub trusted_certifiers: Vec<TrustedCertifier>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletTheme {
pub mode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WalletSettings {
pub trust_settings: TrustSettings,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<WalletTheme>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
}
pub fn default_settings() -> WalletSettings {
WalletSettings {
trust_settings: TrustSettings {
trust_level: 2,
trusted_certifiers: vec![
TrustedCertifier {
name: "Metanet Trust Services".to_string(),
description: "Registry for protocols, baskets, and certificates types"
.to_string(),
icon_url: Some("https://bsvblockchain.org/favicon.ico".to_string()),
identity_key:
"03daf815fe38f83da0ad83b5bedc520aa488aef5cbc93a93c67a7fe60406cbffe8"
.to_string(),
trust: 4,
base_url: None,
},
TrustedCertifier {
name: "SocialCert".to_string(),
description: "Certifies social media handles, phone numbers and emails"
.to_string(),
icon_url: Some("https://socialcert.net/favicon.ico".to_string()),
trust: 3,
identity_key:
"02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17"
.to_string(),
base_url: None,
},
],
},
theme: Some(WalletTheme {
mode: "dark".to_string(),
}),
currency: None,
permission_mode: Some("simple".to_string()),
}
}
struct CacheEntry<T> {
value: T,
expires_at: Instant,
}
impl<T> CacheEntry<T> {
fn new(value: T, ttl: Duration) -> Self {
CacheEntry {
value,
expires_at: Instant::now() + ttl,
}
}
fn is_fresh(&self) -> bool {
Instant::now() < self.expires_at
}
}
pub struct WalletSettingsManager {
storage: Arc<dyn WalletStorageProvider>,
cache: Mutex<Option<CacheEntry<WalletSettings>>>,
default_settings: WalletSettings,
}
impl WalletSettingsManager {
pub fn new(storage: Arc<dyn WalletStorageProvider>) -> Self {
Self::with_defaults(storage, default_settings())
}
pub fn with_defaults(
storage: Arc<dyn WalletStorageProvider>,
default_settings: WalletSettings,
) -> Self {
WalletSettingsManager {
storage,
cache: Mutex::new(None),
default_settings,
}
}
pub async fn get(&self) -> WalletResult<WalletSettings> {
let mut cache = self.cache.lock().await;
if let Some(ref entry) = *cache {
if entry.is_fresh() {
return Ok(entry.value.clone());
}
}
let args = FindSettingsArgs::default();
let rows = self.storage.find_settings_storage(&args).await?;
let settings = if let Some(row) = rows.into_iter().next() {
match row.wallet_settings_json {
Some(ref json) if !json.is_empty() => {
match serde_json::from_str::<WalletSettings>(json) {
Ok(s) => s,
Err(e) => {
warn!(
error = %e,
"Stored walletSettingsJson is invalid JSON; using defaults"
);
self.default_settings.clone()
}
}
}
_ => self.default_settings.clone(),
}
} else {
self.default_settings.clone()
};
*cache = Some(CacheEntry::new(settings.clone(), CACHE_TTL));
Ok(settings)
}
pub async fn set(&self, settings: WalletSettings) -> WalletResult<()> {
let json = serde_json::to_string(&settings).map_err(|e| {
crate::error::WalletError::Internal(format!("Failed to serialize WalletSettings: {e}"))
})?;
let update = SettingsPartial {
wallet_settings_json: Some(json),
..Default::default()
};
self.storage.update_settings_storage(&update).await?;
let mut cache = self.cache.lock().await;
*cache = Some(CacheEntry::new(settings, CACHE_TTL));
Ok(())
}
pub async fn invalidate_cache(&self) {
let mut cache = self.cache.lock().await;
*cache = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_settings_has_two_certifiers() {
let settings = default_settings();
assert_eq!(settings.trust_settings.trusted_certifiers.len(), 2);
assert_eq!(settings.trust_settings.trust_level, 2);
}
#[test]
fn test_settings_serialization_roundtrip() {
let settings = default_settings();
let json = serde_json::to_string(&settings).unwrap();
let deserialized: WalletSettings = serde_json::from_str(&json).unwrap();
assert_eq!(
deserialized.trust_settings.trusted_certifiers.len(),
settings.trust_settings.trusted_certifiers.len()
);
}
}