use std::{fmt, fmt::Debug, fs, path::PathBuf};
use alloy::primitives::Address;
use directories::BaseDirs;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::error::Error;
pub enum FileFormat {
TOML,
YAML,
}
pub trait DiskInterface
where
Self: Sized + Debug + Default + Serialize + DeserializeOwned,
{
const FILE_NAME: &'static str;
const FORMAT: FileFormat;
fn path() -> crate::Result<PathBuf> {
let dirs =
BaseDirs::new().ok_or(Error::InternalErrorStr("Failed to get base directories"))?;
let path = dirs
.home_dir()
.join(".gm")
.join(Self::FILE_NAME)
.with_extension(match Self::FORMAT {
FileFormat::TOML => "toml".to_string(),
FileFormat::YAML => "yaml".to_string(),
});
Ok(path)
}
fn load() -> crate::Result<Self> {
let path = Self::path()?;
if path.exists() {
let content = fs::read_to_string(&path)?;
match Self::FORMAT {
FileFormat::TOML => toml::from_str(&content).map_err(Error::from),
FileFormat::YAML => serde_yaml::from_str(&content).map_err(Error::from),
}
.map_err(|err| {
Error::DiskError(format!(
"Err({err:?}) while deserializing content at {path:?}"
))
})
} else {
Ok(Self::default())
}
}
fn save(&self) -> crate::Result<()> {
let path = Self::path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?; }
let content = match Self::FORMAT {
FileFormat::TOML => toml::to_string_pretty(self).map_err(Error::from),
FileFormat::YAML => serde_yaml::to_string(self).map_err(Error::from),
}
.map_err(|err| {
Error::DiskError(format!("Err({err:?}) while serializing {path:?}: {self:?}"))
})?;
fs::write(path, content)?;
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct AddressBook {
entries: Vec<AddressBookEntry>,
}
impl DiskInterface for AddressBook {
const FILE_NAME: &'static str = "address_book";
const FORMAT: FileFormat = FileFormat::YAML;
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct AddressBookEntry {
pub name: String,
pub address: Address,
}
impl AddressBook {
pub fn add(&mut self, entry: AddressBookEntry) -> Result<(), Error> {
if self.find_by_name(&entry.name).is_some() {
return Err(Error::AddressBook("Name already exists in the addressbook"));
}
if self.find_by_address(&entry.address).is_some() {
return Err(Error::AddressBook(
"Address already exists in the addressbook",
));
}
self.entries.push(entry);
self.save()?;
Ok(())
}
pub fn remove(&mut self, index: usize) -> crate::Result<()> {
self.entries.remove(index);
self.save()
}
pub fn find_by_address(&self, address: &Address) -> Option<(usize, AddressBookEntry)> {
self.entries.iter().enumerate().find_map(|(index, entry)| {
if &entry.address == address {
Some((index, entry.clone()))
} else {
None
}
})
}
pub fn find_by_name(&self, name: &str) -> Option<(usize, AddressBookEntry)> {
self.entries.iter().enumerate().find_map(|(index, entry)| {
if entry.name == name {
Some((index, entry.clone()))
} else {
None
}
})
}
pub fn find(
&self,
id: &Option<usize>,
address: &Option<Address>,
name: &Option<&String>,
) -> crate::Result<Option<(usize, AddressBookEntry)>> {
if let Some(address) = address {
Ok(self.find_by_address(address))
} else if let Some(name) = name {
Ok(self.find_by_name(name))
} else if let Some(id) = id {
let index = *id - 1;
let entry = AddressBook::load()?.list()[index].clone();
Ok(Some((*id, entry)))
} else {
Ok(None)
}
}
pub fn update(&mut self, id: usize, new_entry: AddressBookEntry) -> crate::Result<()> {
self.entries[id - 1] = new_entry;
self.save()
}
pub fn list(&self) -> &Vec<AddressBookEntry> {
&self.entries
}
pub fn list_owned(self) -> Vec<AddressBookEntry> {
self.entries
}
pub fn load_list() -> crate::Result<Vec<AddressBookEntry>> {
Ok(AddressBook::load()?.list_owned())
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Config {
pub current_account: Option<Address>,
pub testnet_mode: bool,
#[serde(default)]
pub developer_mode: bool,
pub alchemy_api_key: Option<String>,
#[serde(default = "default_theme_name")]
pub theme_name: String,
}
fn default_theme_name() -> String {
"Monochrome".to_string()
}
impl DiskInterface for Config {
const FILE_NAME: &'static str = "config";
const FORMAT: FileFormat = FileFormat::TOML;
}
impl Config {
pub fn current_account() -> crate::Result<Option<Address>> {
Ok(Config::load()?.current_account)
}
pub fn try_current_account(&self) -> crate::Result<Address> {
self.current_account
.ok_or_else(|| crate::Error::CurrentAccountNotSet)
}
pub fn set_current_account(address: Address) -> crate::Result<()> {
let mut config = Config::load()?;
config.current_account = Some(address);
config.save()?;
Ok(())
}
pub fn alchemy_api_key() -> crate::Result<String> {
Config::load()?
.alchemy_api_key
.ok_or(crate::Error::AlchemyApiKeyNotSet)
}
pub fn set_alchemy_api_key(alchemy_api_key: String) -> crate::Result<()> {
let mut config = Config::load()?;
config.alchemy_api_key = Some(alchemy_api_key);
config.save()?;
Ok(())
}
}
impl fmt::Display for AddressBookEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name, self.address)
}
}