zilliqa_rs/signers/
local_wallet.rs

1use std::{path::Path, str::FromStr};
2
3use eth_keystore::{decrypt_key, encrypt_key};
4use k256::ecdsa::Signature;
5
6use crate::{
7    core::{PrivateKey, PublicKey, ZilAddress},
8    crypto::schnorr::sign,
9    Error,
10};
11
12use super::Signer;
13
14/// Represents a local wallet, containing a private key, address, and public key.
15#[derive(Debug, Clone, PartialEq)]
16pub struct LocalWallet {
17    /// Private key of the wallet.
18    pub private_key: PrivateKey,
19    /// Public address of the wallet which is used to receive ZIL.
20    pub address: ZilAddress,
21    /// Public key of the wallet.
22    public_key: PublicKey,
23}
24
25impl LocalWallet {
26    /// Generates a random private key and creates a new instance of a LocalWallet
27    /// using that key.
28    ///
29    /// # Example
30    /// ```
31    /// use zilliqa_rs::signers::LocalWallet;
32    /// let wallet = LocalWallet::create_random().unwrap();
33    /// ```
34    pub fn create_random() -> Result<Self, Error> {
35        PrivateKey::create_random().try_into()
36    }
37
38    /// Loads a wallet from a keystore file.
39    ///
40    /// # Example
41    /// ```
42    /// use zilliqa_rs::signers::LocalWallet;
43    /// use std::path::Path;
44    /// let wallet = LocalWallet::load_keystore(&Path::new("./tests/keystore.json"), "zxcvbnm,").unwrap();
45    /// ```
46    pub fn load_keystore(path: &Path, password: &str) -> Result<Self, Error> {
47        PrivateKey::from_slice(&decrypt_key(path, password).unwrap())?.try_into()
48    }
49
50    /// Encrypts the given wallet using the Scrypt password-based key derivation function, and stores it in the provided path. On success, it returns the id (Uuid) generated for this keystore.
51    ///
52    /// # Example
53    /// ```
54    /// use zilliqa_rs::signers::LocalWallet;
55    /// use std::path::Path;
56    /// use std::env;
57    ///
58    /// let wallet = LocalWallet::create_random().unwrap();
59    /// let path = env::temp_dir().join("test_keystore.json");
60    /// let filename = wallet.save_keystore(&path, "zxcvbnm,").unwrap();
61    /// ```
62    pub fn save_keystore(&self, path: &Path, password: &str) -> Result<String, Error> {
63        let mut rng = rand::thread_rng();
64        if path.is_dir() {
65            return Err(Error::IsADirectory);
66        }
67
68        let (dir, filename) = (
69            path.parent().ok_or(Error::FailedToGetTheParentDirectory)?,
70            path.file_name().map(|filename| filename.to_str().unwrap()),
71        );
72        Ok(encrypt_key(dir, &mut rng, self.private_key.to_bytes(), password, filename)?)
73    }
74}
75
76impl Signer for LocalWallet {
77    fn sign(&self, message: &[u8]) -> Signature {
78        sign(message, &self.private_key)
79    }
80
81    fn address(&self) -> &ZilAddress {
82        &self.address
83    }
84
85    fn public_key(&self) -> &PublicKey {
86        &self.public_key
87    }
88}
89
90impl FromStr for LocalWallet {
91    type Err = Error;
92
93    /// Create a new LocalWallet out of a string slice containing a private key.
94    fn from_str(private_key: &str) -> Result<Self, Self::Err> {
95        private_key.parse::<PrivateKey>()?.try_into()
96    }
97}
98
99impl TryFrom<PrivateKey> for LocalWallet {
100    type Error = Error;
101
102    /// Converts a private key to a LocalWallet.
103    ///
104    /// # Example
105    /// ```
106    /// use zilliqa_rs::signers::LocalWallet;
107    /// use zilliqa_rs::core::PrivateKey;
108    ///
109    /// let private_key = PrivateKey::create_random();
110    /// let wallet = LocalWallet::try_from(private_key).unwrap();
111    /// ```
112    fn try_from(private_key: PrivateKey) -> Result<Self, Error> {
113        let address = ZilAddress::try_from(&private_key.public_key())?;
114
115        Ok(Self {
116            address,
117            public_key: private_key.public_key(),
118            private_key,
119        })
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use std::env;
126
127    use claim::assert_some;
128
129    use crate::{core::ZilAddress, crypto::schnorr::verify, signers::Signer};
130
131    use super::LocalWallet;
132
133    #[test]
134    fn a_valid_private_key_should_results_a_valid_account_with_parse_function() {
135        let account: LocalWallet = "0xD96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba"
136            .parse()
137            .unwrap();
138        assert_eq!(
139            account.address(),
140            &"0x381f4008505e940AD7681EC3468a719060caF796".parse::<ZilAddress>().unwrap()
141        );
142    }
143
144    #[test]
145    fn a_valid_private_key_should_results_a_valid_account_with_new() {
146        let account: LocalWallet = "0xD96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba"
147            .parse()
148            .unwrap();
149        assert_eq!(
150            account.address(),
151            &"0x381f4008505e940AD7681EC3468a719060caF796".parse::<ZilAddress>().unwrap()
152        );
153    }
154
155    #[test]
156    fn sign_should_return_signature() {
157        let account: LocalWallet = "0xD96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba"
158            .parse()
159            .unwrap();
160
161        let signature = account.sign(&hex::decode("11223344aabb").unwrap());
162        println!("{} {}", signature.r().to_string(), signature.s().to_string());
163
164        assert_some!(verify(
165            &hex::decode("11223344aabb").unwrap(),
166            &account.public_key(),
167            &signature
168        ));
169    }
170
171    #[test]
172    fn save_and_load_keystore_should_work_fine() {
173        let wallet = LocalWallet::create_random().unwrap();
174        let path = env::temp_dir().join("keystore.json");
175        let password = "qwerty";
176        wallet.save_keystore(&path, password).unwrap();
177
178        let wallet2 = LocalWallet::load_keystore(&path, password).unwrap();
179        assert_eq!(wallet.address, wallet2.address);
180    }
181}