1use std::collections::BTreeMap;
5use std::fmt::{Display, Formatter, Write};
6use std::fs::File;
7use std::io::BufReader;
8use std::path::PathBuf;
9
10use enum_dispatch::enum_dispatch;
11use eyre::{Context as _, bail, eyre};
12use fastcrypto::traits::EncodeDecodeBase64;
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14use sui_sdk_types::Address as SuiAddress;
15
16use crate::crypto::{PublicKey, Signature, SuiKeyPair};
17use crate::intent::{Intent, IntentMessage};
18
19pub type Error = eyre::Report;
20
21#[derive(Serialize, Deserialize)]
22#[enum_dispatch(ReadOnlyAccountKeystore)]
23pub enum Keystore {
24 File(FileBasedKeystore),
25 InMem(InMemKeystore),
26}
27
28impl Display for Keystore {
29 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
30 let mut writer = String::new();
31 match self {
32 Self::File(file) => {
33 writeln!(writer, "Keystore Type : File")?;
34 write!(writer, "Keystore Path : {:?}", file.path)?;
35 write!(f, "{}", writer)
36 }
37 Self::InMem(_) => {
38 writeln!(writer, "Keystore Type : InMem")?;
39 write!(f, "{}", writer)
40 }
41 }
42 }
43}
44
45#[enum_dispatch]
51pub trait ReadOnlyAccountKeystore: Send + Sync {
52 fn keys(&self) -> Vec<PublicKey>;
53
54 fn get_key(&self, address: &SuiAddress) -> Result<&SuiKeyPair, Error>;
55
56 fn sign_hashed(&self, address: &SuiAddress, msg: &[u8]) -> Result<Signature, signature::Error>;
57
58 fn sign_secure<T>(
59 &self,
60 address: &SuiAddress,
61 msg: &T,
62 intent: Intent,
63 ) -> Result<Signature, signature::Error>
64 where
65 T: Serialize;
66
67 fn addresses(&self) -> Vec<SuiAddress> {
68 self.keys().iter().map(|k| k.to_sui_address()).collect()
69 }
70
71 fn addresses_with_alias(&self) -> Vec<(&SuiAddress, &Alias)>;
72
73 fn aliases(&self) -> Vec<&Alias>;
74
75 fn alias_names(&self) -> Vec<&str> {
76 self.aliases()
77 .into_iter()
78 .map(|a| a.alias.as_str())
79 .collect()
80 }
81
82 fn get_alias_by_address(&self, address: &SuiAddress) -> Result<String, Error>;
84
85 fn get_address_by_alias(&self, alias: String) -> Result<&SuiAddress, Error>;
86
87 fn alias_exists(&self, alias: &str) -> bool {
89 self.alias_names().contains(&alias)
90 }
91}
92
93#[derive(Default)]
94pub struct FileBasedKeystore {
95 keys: BTreeMap<SuiAddress, SuiKeyPair>,
96 aliases: BTreeMap<SuiAddress, Alias>,
97 path: Option<PathBuf>,
98}
99
100impl FileBasedKeystore {
101 pub fn new(path: PathBuf) -> Result<Self, Error> {
102 let keys = if path.exists() {
103 let reader =
104 BufReader::new(File::open(&path).with_context(|| {
105 format!("Cannot open the keystore file: {}", path.display())
106 })?);
107 let kp_strings: Vec<String> = serde_json::from_reader(reader).with_context(|| {
108 format!("Cannot deserialize the keystore file: {}", path.display(),)
109 })?;
110 kp_strings
111 .iter()
112 .map(|kpstr| {
113 let key = SuiKeyPair::decode_base64(kpstr);
114 key.map(|k| (k.public().to_sui_address(), k))
115 })
116 .collect::<Result<BTreeMap<_, _>, _>>()
117 .map_err(|e| eyre!("Invalid keystore file: {}. {}", path.display(), e))?
118 } else {
119 BTreeMap::new()
120 };
121
122 let mut aliases_path = path.clone();
124 aliases_path.set_extension("aliases");
125
126 let aliases = if aliases_path.exists() {
127 let reader = BufReader::new(File::open(&aliases_path).with_context(|| {
128 format!(
129 "Cannot open aliases file in keystore: {}",
130 aliases_path.display()
131 )
132 })?);
133
134 let aliases: Vec<Alias> = serde_json::from_reader(reader).with_context(|| {
135 format!(
136 "Cannot deserialize aliases file in keystore: {}",
137 aliases_path.display(),
138 )
139 })?;
140
141 aliases
142 .into_iter()
143 .map(|alias| {
144 let key = PublicKey::decode_base64(&alias.public_key_base64);
145 key.map(|k| (k.to_sui_address(), alias))
146 })
147 .collect::<Result<BTreeMap<_, _>, _>>()
148 .map_err(|e| {
149 eyre!(
150 "Invalid aliases file in keystore: {}. {}",
151 aliases_path.display(),
152 e
153 )
154 })?
155 } else {
156 BTreeMap::new()
157 };
158
159 Ok(Self {
160 keys,
161 aliases,
162 path: Some(path),
163 })
164 }
165
166 pub fn key_pairs(&self) -> Vec<&SuiKeyPair> {
167 self.keys.values().collect()
168 }
169}
170
171impl Serialize for FileBasedKeystore {
172 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173 where
174 S: Serializer,
175 {
176 let path_default = PathBuf::default();
177 serializer.serialize_str(
178 self.path
179 .as_ref()
180 .unwrap_or(&path_default)
181 .to_str()
182 .unwrap_or(""),
183 )
184 }
185}
186
187impl<'de> Deserialize<'de> for FileBasedKeystore {
188 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189 where
190 D: Deserializer<'de>,
191 {
192 use serde::de::Error;
193 Self::new(PathBuf::from(String::deserialize(deserializer)?)).map_err(D::Error::custom)
194 }
195}
196
197impl ReadOnlyAccountKeystore for FileBasedKeystore {
198 fn sign_hashed(&self, address: &SuiAddress, msg: &[u8]) -> Result<Signature, signature::Error> {
199 Ok(Signature::new_hashed(
200 msg,
201 self.keys.get(address).ok_or_else(|| {
202 signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
203 })?,
204 ))
205 }
206 fn sign_secure<T>(
207 &self,
208 address: &SuiAddress,
209 msg: &T,
210 intent: Intent,
211 ) -> Result<Signature, signature::Error>
212 where
213 T: Serialize,
214 {
215 Ok(Signature::new_secure(
216 &IntentMessage::new(intent, msg),
217 self.keys.get(address).ok_or_else(|| {
218 signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
219 })?,
220 ))
221 }
222
223 fn aliases(&self) -> Vec<&Alias> {
225 self.aliases.values().collect()
226 }
227
228 fn addresses_with_alias(&self) -> Vec<(&SuiAddress, &Alias)> {
229 self.aliases.iter().collect::<Vec<_>>()
230 }
231
232 fn keys(&self) -> Vec<PublicKey> {
233 self.keys.values().map(|key| key.public()).collect()
234 }
235
236 fn get_address_by_alias(&self, alias: String) -> Result<&SuiAddress, Error> {
238 self.addresses_with_alias()
239 .iter()
240 .find(|x| x.1.alias == alias)
241 .ok_or_else(|| eyre!("Cannot resolve alias {alias} to an address"))
242 .map(|x| x.0)
243 }
244
245 fn get_alias_by_address(&self, address: &SuiAddress) -> Result<String, Error> {
247 match self.aliases.get(address) {
248 Some(alias) => Ok(alias.alias.clone()),
249 None => bail!("Cannot find alias for address {address}"),
250 }
251 }
252
253 fn get_key(&self, address: &SuiAddress) -> Result<&SuiKeyPair, Error> {
254 #[allow(clippy::option_if_let_else)]
255 match self.keys.get(address) {
256 Some(key) => Ok(key),
257 None => Err(eyre!("Cannot find key for address: [{address}]")),
258 }
259 }
260}
261
262#[derive(Default, Serialize, Deserialize)]
264pub struct InMemKeystore {
265 aliases: BTreeMap<SuiAddress, Alias>,
266 keys: BTreeMap<SuiAddress, SuiKeyPair>,
267}
268
269impl ReadOnlyAccountKeystore for InMemKeystore {
270 fn sign_hashed(&self, address: &SuiAddress, msg: &[u8]) -> Result<Signature, signature::Error> {
271 Ok(Signature::new_hashed(
272 msg,
273 self.keys.get(address).ok_or_else(|| {
274 signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
275 })?,
276 ))
277 }
278 fn sign_secure<T>(
279 &self,
280 address: &SuiAddress,
281 msg: &T,
282 intent: Intent,
283 ) -> Result<Signature, signature::Error>
284 where
285 T: Serialize,
286 {
287 Ok(Signature::new_secure(
288 &IntentMessage::new(intent, msg),
289 self.keys.get(address).ok_or_else(|| {
290 signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
291 })?,
292 ))
293 }
294
295 fn aliases(&self) -> Vec<&Alias> {
297 self.aliases.values().collect()
298 }
299
300 fn addresses_with_alias(&self) -> Vec<(&SuiAddress, &Alias)> {
301 self.aliases.iter().collect::<Vec<_>>()
302 }
303
304 fn keys(&self) -> Vec<PublicKey> {
305 self.keys.values().map(|key| key.public()).collect()
306 }
307
308 fn get_key(&self, address: &SuiAddress) -> Result<&SuiKeyPair, Error> {
309 #[allow(clippy::option_if_let_else)]
310 match self.keys.get(address) {
311 Some(key) => Ok(key),
312 None => Err(eyre!("Cannot find key for address: [{address}]")),
313 }
314 }
315
316 fn get_alias_by_address(&self, address: &SuiAddress) -> Result<String, Error> {
318 match self.aliases.get(address) {
319 Some(alias) => Ok(alias.alias.clone()),
320 None => bail!("Cannot find alias for address {address}"),
321 }
322 }
323
324 fn get_address_by_alias(&self, alias: String) -> Result<&SuiAddress, Error> {
326 self.addresses_with_alias()
327 .iter()
328 .find(|x| x.1.alias == alias)
329 .ok_or_else(|| eyre!("Cannot resolve alias {alias} to an address"))
330 .map(|x| x.0)
331 }
332}
333
334#[derive(Serialize, Deserialize, Clone, Debug)]
335pub struct Alias {
336 pub alias: String,
337 pub public_key_base64: String,
338}
339
340#[cfg(test)]
341mod tests {
342 use std::io::Write;
343
344 use super::*;
345
346 #[test]
347 fn new_file_keystore() -> eyre::Result<()> {
348 let temp_dir = tempfile::tempdir()?;
349 let path = temp_dir.path();
350 let mut keystore = File::create(path.join("sui.keystore"))?;
351 serde_json::to_writer(
352 &keystore,
353 &serde_json::json!([
354 "AKd4u480uT0eLUNe7vh2zHHYdbpUXY/fwcL13eJQ5/zs",
355 "AI1TKQ0qPLor32rdLOZiN0/J4qNPyypesT1eE+R/wSCB",
356 "AFHMjegm2IwuiLemXb6o7XvuDL7xn1JTHc66CZefYY+B",
357 "APhbsR3gpjBIRvZm5ZwMZhncejgYH/hGa6wHVtaTat22",
358 "ADO8QyYe0MM+HP0iLjHNLPAxZXNYyE1jieny3iN+fDCS",
359 "AKfLSiyx3pUSEpvn0tyY+17ef8AjN7izfQ9qm048BhqM",
360 "AOzplQlAK2Uznvog7xmcMtlFC+DfuJx3axo9lfyI876G",
361 "AI1I9i3mk2e1kAjPnB7fKiqquxc1OjjAkkpQPIk9Id5Q",
362 "AIUAgL5jYMzf0JPCmc263Ou6tH5Z/HuAdtWFFUiz8Zc0",
363 "AFmgBTlVGHfYieuSVmQ63BJ+zQSY8pNOUXH99Ucb1ZGl",
364 "AAu4ySMvq2wygxl/Ze6AGgkYfxg+rzUElj7UxxI6NHBI"
365 ]),
366 )?;
367 keystore.flush()?;
368 let mut aliases = File::create(path.join("sui.aliases"))?;
369 serde_json::to_writer(
370 &aliases,
371 &serde_json::json!([
372 {
373 "alias": "grace",
374 "public_key_base64": "ABhIIE33kaUT1rr9rNrh0XJNb7AC6EBSdh5Ku4a2B7wU"
375 },
376 {
377 "alias": "heidi",
378 "public_key_base64": "ACRAZZ+qMcBA7gJg6iacBSgB4S+DB3nHjk9E1237R4+h"
379 },
380 {
381 "alias": "admin",
382 "public_key_base64": "AONa32KBWXqsu6pksuwCLbA0v3JoSPbw8du45Rkw14nm"
383 },
384 {
385 "alias": "ivan",
386 "public_key_base64": "AKsTkJa8fJg2PJtUTUxIE+FHBBG6IFkHk4385yehR86L"
387 },
388 {
389 "alias": "judy",
390 "public_key_base64": "AEIcS8FhN0CjRUGjVHNmXOW6Rb+ootVN3a4kEbBoQ4R6"
391 },
392 {
393 "alias": "eve",
394 "public_key_base64": "AP0TE5MM1h7QSZrnlBcdQepKA/6Fh5pja3gjMNpL1fix"
395 },
396 {
397 "alias": "alice",
398 "public_key_base64": "AK9WofTFdyBcMpMxzYkbgNQiKLgr9qH8iz9ON6VFxwiW"
399 },
400 {
401 "alias": "bob",
402 "public_key_base64": "ALieneYHseSZILiNAda3z29Ob4lZKBAr3jEyP41WsJAG"
403 },
404 {
405 "alias": "charlie",
406 "public_key_base64": "ABm2kTdq/96JsbsTMunKZDqJbIsEa1lwIJ0cA2CJ4z5l"
407 },
408 {
409 "alias": "frank",
410 "public_key_base64": "ADSxYutFskDwLNnEto/E+KDJe4QXWHkO7d8Ha6nqBR0/"
411 },
412 {
413 "alias": "dave",
414 "public_key_base64": "ALmzETq2T6c06a+VXJzx1pkfuLBVetRs5q537l6UO4KI"
415 }
416 ]),
417 )?;
418 aliases.flush()?;
419 let keystore = FileBasedKeystore::new(path.join("sui.keystore"))?;
420 assert!(!keystore.key_pairs().is_empty());
421 assert!(
422 keystore
423 .get_key(
424 &"0x98e9cafb116af9d69f77ce0d644c60e384f850f8af050b268377d8293d7fe7c6"
425 .parse()?
426 )
427 .is_ok()
428 );
429 assert!(keystore.get_address_by_alias("alice".to_owned()).is_ok());
430 Ok(())
431 }
432
433 #[test]
434 fn new_file_keystore_no_aliases() -> eyre::Result<()> {
435 let temp_dir = tempfile::tempdir()?;
436 let path = temp_dir.path();
437 let keystore_path = path.join("sui.keystore");
438 serde_json::to_writer(
439 File::create(keystore_path.clone())?,
440 &serde_json::json!([
441 "AKd4u480uT0eLUNe7vh2zHHYdbpUXY/fwcL13eJQ5/zs",
442 "AI1TKQ0qPLor32rdLOZiN0/J4qNPyypesT1eE+R/wSCB",
443 "AFHMjegm2IwuiLemXb6o7XvuDL7xn1JTHc66CZefYY+B",
444 "APhbsR3gpjBIRvZm5ZwMZhncejgYH/hGa6wHVtaTat22",
445 "ADO8QyYe0MM+HP0iLjHNLPAxZXNYyE1jieny3iN+fDCS",
446 "AKfLSiyx3pUSEpvn0tyY+17ef8AjN7izfQ9qm048BhqM",
447 "AOzplQlAK2Uznvog7xmcMtlFC+DfuJx3axo9lfyI876G",
448 "AI1I9i3mk2e1kAjPnB7fKiqquxc1OjjAkkpQPIk9Id5Q",
449 "AIUAgL5jYMzf0JPCmc263Ou6tH5Z/HuAdtWFFUiz8Zc0",
450 "AFmgBTlVGHfYieuSVmQ63BJ+zQSY8pNOUXH99Ucb1ZGl",
451 "AAu4ySMvq2wygxl/Ze6AGgkYfxg+rzUElj7UxxI6NHBI"
452 ]),
453 )?;
454 let keystore = FileBasedKeystore::new(keystore_path)?;
455 assert!(!keystore.key_pairs().is_empty());
456 assert!(
457 keystore
458 .get_key(
459 &"0x98e9cafb116af9d69f77ce0d644c60e384f850f8af050b268377d8293d7fe7c6"
460 .parse()?
461 )
462 .is_ok()
463 );
464 assert!(keystore.get_address_by_alias("alice".to_owned()).is_err());
465 Ok(())
466 }
467}