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 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 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 fn save(&self) -> crate::Result<()> {
58 let path = Self::path()?;
59
60 if let Some(parent) = path.parent() {
61 fs::create_dir_all(parent)?; }
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 }
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
226impl fmt::Display for AddressBookEntry {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 write!(f, "{} ({})", self.name, self.address)
230 }
231}