Skip to main content

gm_lib/
disk.rs

1use std::{fmt, fmt::Debug, fs, path::PathBuf};
2
3use alloy::primitives::Address;
4use directories::BaseDirs;
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6
7use crate::error::Error;
8
9pub enum FileFormat {
10    TOML,
11    YAML,
12}
13
14pub trait DiskInterface
15where
16    Self: Sized + Debug + Default + Serialize + DeserializeOwned,
17{
18    const FILE_NAME: &'static str;
19    const FORMAT: FileFormat;
20
21    /// Get the path to the file
22    fn path() -> crate::Result<PathBuf> {
23        let dirs =
24            BaseDirs::new().ok_or(Error::InternalErrorStr("Failed to get base directories"))?;
25        let path = dirs
26            .home_dir()
27            .join(".gm")
28            .join(Self::FILE_NAME)
29            .with_extension(match Self::FORMAT {
30                FileFormat::TOML => "toml".to_string(),
31                FileFormat::YAML => "yaml".to_string(),
32            });
33        Ok(path)
34    }
35
36    /// Load the content from the file
37    fn load() -> crate::Result<Self> {
38        let path = Self::path()?;
39
40        if path.exists() {
41            let content = fs::read_to_string(&path)?;
42            match Self::FORMAT {
43                FileFormat::TOML => toml::from_str(&content).map_err(Error::from),
44                FileFormat::YAML => serde_yaml::from_str(&content).map_err(Error::from),
45            }
46            .map_err(|err| {
47                Error::DiskError(format!(
48                    "Err({err:?}) while deserializing content at {path:?}"
49                ))
50            })
51        } else {
52            Ok(Self::default())
53        }
54    }
55
56    /// Save settings to a file
57    fn save(&self) -> crate::Result<()> {
58        let path = Self::path()?;
59
60        if let Some(parent) = path.parent() {
61            fs::create_dir_all(parent)?; // Ensure config directory exists
62        }
63
64        let content = match Self::FORMAT {
65            FileFormat::TOML => toml::to_string_pretty(self).map_err(Error::from),
66            FileFormat::YAML => serde_yaml::to_string(self).map_err(Error::from),
67        }
68        .map_err(|err| {
69            Error::DiskError(format!("Err({err:?}) while serializing {path:?}: {self:?}"))
70        })?;
71
72        fs::write(path, content)?;
73
74        Ok(())
75    }
76}
77
78#[derive(Serialize, Deserialize, Debug, Default)]
79pub struct AddressBook {
80    entries: Vec<AddressBookEntry>,
81}
82
83impl DiskInterface for AddressBook {
84    const FILE_NAME: &'static str = "address_book";
85    const FORMAT: FileFormat = FileFormat::YAML;
86}
87
88#[derive(Serialize, Deserialize, Debug, Default, Clone)]
89pub struct AddressBookEntry {
90    pub name: String,
91    pub address: Address,
92    // TODO we can add more fields here like last interacted time
93}
94
95impl AddressBook {
96    pub fn add(&mut self, entry: AddressBookEntry) -> Result<(), Error> {
97        if self.find_by_name(&entry.name).is_some() {
98            return Err(Error::AddressBook("Name already exists in the addressbook"));
99        }
100
101        if self.find_by_address(&entry.address).is_some() {
102            return Err(Error::AddressBook(
103                "Address already exists in the addressbook",
104            ));
105        }
106
107        self.entries.push(entry);
108        self.save()?;
109
110        Ok(())
111    }
112
113    pub fn remove(&mut self, index: usize) -> crate::Result<()> {
114        self.entries.remove(index);
115        self.save()
116    }
117
118    pub fn find_by_address(&self, address: &Address) -> Option<(usize, AddressBookEntry)> {
119        self.entries.iter().enumerate().find_map(|(index, entry)| {
120            if &entry.address == address {
121                Some((index, entry.clone()))
122            } else {
123                None
124            }
125        })
126    }
127
128    pub fn find_by_name(&self, name: &str) -> Option<(usize, AddressBookEntry)> {
129        self.entries.iter().enumerate().find_map(|(index, entry)| {
130            if entry.name == name {
131                Some((index, entry.clone()))
132            } else {
133                None
134            }
135        })
136    }
137
138    pub fn find(
139        &self,
140        id: &Option<usize>,
141        address: &Option<Address>,
142        name: &Option<&String>,
143    ) -> crate::Result<Option<(usize, AddressBookEntry)>> {
144        if let Some(address) = address {
145            Ok(self.find_by_address(address))
146        } else if let Some(name) = name {
147            Ok(self.find_by_name(name))
148        } else if let Some(id) = id {
149            let index = *id - 1;
150            let entry = AddressBook::load()?.list()[index].clone();
151            Ok(Some((*id, entry)))
152        } else {
153            Ok(None)
154        }
155    }
156
157    pub fn update(&mut self, id: usize, new_entry: AddressBookEntry) -> crate::Result<()> {
158        self.entries[id - 1] = new_entry;
159        self.save()
160    }
161
162    pub fn list(&self) -> &Vec<AddressBookEntry> {
163        &self.entries
164    }
165
166    pub fn list_owned(self) -> Vec<AddressBookEntry> {
167        self.entries
168    }
169
170    pub fn load_list() -> crate::Result<Vec<AddressBookEntry>> {
171        Ok(AddressBook::load()?.list_owned())
172    }
173}
174
175#[derive(Serialize, Deserialize, Debug, Default)]
176pub struct Config {
177    pub current_account: Option<Address>,
178    pub testnet_mode: bool,
179    #[serde(default)]
180    pub developer_mode: bool,
181    pub alchemy_api_key: Option<String>,
182    #[serde(default = "default_theme_name")]
183    pub theme_name: String,
184}
185
186fn default_theme_name() -> String {
187    "Monochrome".to_string()
188}
189
190impl DiskInterface for Config {
191    const FILE_NAME: &'static str = "config";
192    const FORMAT: FileFormat = FileFormat::TOML;
193}
194
195impl Config {
196    pub fn current_account() -> crate::Result<Option<Address>> {
197        Ok(Config::load()?.current_account)
198    }
199
200    pub fn try_current_account(&self) -> crate::Result<Address> {
201        self.current_account
202            .ok_or_else(|| crate::Error::CurrentAccountNotSet)
203    }
204
205    pub fn set_current_account(address: Address) -> crate::Result<()> {
206        let mut config = Config::load()?;
207        config.current_account = Some(address);
208        config.save()?;
209        Ok(())
210    }
211
212    pub fn alchemy_api_key() -> crate::Result<String> {
213        Config::load()?
214            .alchemy_api_key
215            .ok_or(crate::Error::AlchemyApiKeyNotSet)
216    }
217
218    pub fn set_alchemy_api_key(alchemy_api_key: String) -> crate::Result<()> {
219        let mut config = Config::load()?;
220        config.alchemy_api_key = Some(alchemy_api_key);
221        config.save()?;
222        Ok(())
223    }
224}
225
226// Implementing Display for AddressBookEntry to format how entries appear in selections.
227impl fmt::Display for AddressBookEntry {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        write!(f, "{} ({})", self.name, self.address)
230    }
231}