Skip to main content

soroban_cli/config/
key.rs

1use std::{fmt::Display, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5use super::secret::{self, Secret};
6use crate::xdr;
7
8#[derive(thiserror::Error, Debug)]
9pub enum Error {
10    #[error("failed to extract secret from public key ")]
11    SecretPublicKey,
12    #[error(transparent)]
13    Secret(#[from] secret::Error),
14    #[error(transparent)]
15    StrKey(#[from] stellar_strkey::DecodeError),
16    #[error("failed to parse key")]
17    Parse,
18    #[error("expected a public key (G...) or muxed account (M...)")]
19    PublicKeyExpected,
20}
21
22#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
23pub enum Key {
24    #[serde(rename = "public_key")]
25    PublicKey(Public),
26    #[serde(rename = "muxed_account")]
27    MuxedAccount(MuxedAccount),
28    #[serde(untagged)]
29    Secret(Secret),
30}
31
32impl Key {
33    pub fn muxed_account(&self, hd_path: Option<u32>) -> Result<xdr::MuxedAccount, Error> {
34        let bytes = match self {
35            Key::Secret(secret) => secret.public_key(hd_path)?.0,
36            Key::PublicKey(Public(key)) => key.0,
37            Key::MuxedAccount(MuxedAccount(stellar_strkey::ed25519::MuxedAccount {
38                ed25519,
39                id,
40            })) => {
41                return Ok(xdr::MuxedAccount::MuxedEd25519(xdr::MuxedAccountMed25519 {
42                    ed25519: xdr::Uint256(*ed25519),
43                    id: *id,
44                }))
45            }
46        };
47        Ok(xdr::MuxedAccount::Ed25519(xdr::Uint256(bytes)))
48    }
49
50    pub fn private_key(
51        &self,
52        hd_path: Option<u32>,
53    ) -> Result<stellar_strkey::ed25519::PrivateKey, Error> {
54        match self {
55            Key::Secret(secret) => Ok(secret.private_key(hd_path)?),
56            _ => Err(Error::SecretPublicKey),
57        }
58    }
59
60    pub fn parse_public_only(s: &str) -> Result<Self, Error> {
61        match s.parse()? {
62            key @ (Key::PublicKey(_) | Key::MuxedAccount(_)) => Ok(key),
63            Key::Secret(_) => Err(Error::PublicKeyExpected),
64        }
65    }
66}
67
68impl FromStr for Key {
69    type Err = Error;
70
71    fn from_str(s: &str) -> Result<Self, Self::Err> {
72        if let Ok(secret) = s.parse() {
73            return Ok(Key::Secret(secret));
74        }
75        if let Ok(public_key) = s.parse() {
76            return Ok(Key::PublicKey(public_key));
77        }
78        if let Ok(muxed_account) = s.parse() {
79            return Ok(Key::MuxedAccount(muxed_account));
80        }
81        Err(Error::Parse)
82    }
83}
84
85impl From<stellar_strkey::ed25519::PublicKey> for Key {
86    fn from(value: stellar_strkey::ed25519::PublicKey) -> Self {
87        Key::PublicKey(Public(value))
88    }
89}
90
91impl From<&stellar_strkey::ed25519::PublicKey> for Key {
92    fn from(stellar_strkey::ed25519::PublicKey(key): &stellar_strkey::ed25519::PublicKey) -> Self {
93        stellar_strkey::ed25519::PublicKey(*key).into()
94    }
95}
96
97#[derive(Debug, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
98pub struct Public(pub stellar_strkey::ed25519::PublicKey);
99
100impl FromStr for Public {
101    type Err = stellar_strkey::DecodeError;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        Ok(Public(stellar_strkey::ed25519::PublicKey::from_str(s)?))
105    }
106}
107
108impl Display for Public {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "{}", self.0)
111    }
112}
113
114impl From<&Public> for stellar_strkey::ed25519::MuxedAccount {
115    fn from(Public(stellar_strkey::ed25519::PublicKey(key)): &Public) -> Self {
116        stellar_strkey::ed25519::MuxedAccount {
117            id: 0,
118            ed25519: *key,
119        }
120    }
121}
122
123#[derive(Debug, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
124pub struct MuxedAccount(pub stellar_strkey::ed25519::MuxedAccount);
125
126impl FromStr for MuxedAccount {
127    type Err = stellar_strkey::DecodeError;
128
129    fn from_str(s: &str) -> Result<Self, Self::Err> {
130        Ok(MuxedAccount(
131            stellar_strkey::ed25519::MuxedAccount::from_str(s)?,
132        ))
133    }
134}
135
136impl Display for MuxedAccount {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        write!(f, "{}", self.0)
139    }
140}
141
142impl TryFrom<Key> for Secret {
143    type Error = Error;
144
145    fn try_from(key: Key) -> Result<Secret, Self::Error> {
146        match key {
147            Key::Secret(secret) => Ok(secret),
148            _ => Err(Error::SecretPublicKey),
149        }
150    }
151}
152
153#[cfg(test)]
154mod test {
155    use super::*;
156
157    fn round_trip(key: &Key) {
158        let serialized = toml::to_string(&key).unwrap();
159        println!("{serialized}");
160        let deserialized: Key = toml::from_str(&serialized).unwrap();
161        assert_eq!(key, &deserialized);
162    }
163
164    #[test]
165    fn public_key() {
166        let key = Key::PublicKey(Public(stellar_strkey::ed25519::PublicKey([0; 32])));
167        round_trip(&key);
168    }
169    #[test]
170    fn muxed_key() {
171        let key: stellar_strkey::ed25519::MuxedAccount =
172            "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU"
173                .parse()
174                .unwrap();
175        let key = Key::MuxedAccount(MuxedAccount(key));
176        round_trip(&key);
177    }
178    #[test]
179    fn secret_key() {
180        let secret_key = format!("{}", stellar_strkey::ed25519::PrivateKey([0; 32]));
181        let secret = Secret::SecretKey { secret_key };
182        let key = Key::Secret(secret);
183        round_trip(&key);
184    }
185    #[test]
186    fn secret_seed_phrase() {
187        let seed_phrase = "singer swing mango apple singer swing mango apple singer swing mango apple singer swing mango apple".to_string();
188        let secret = Secret::SeedPhrase {
189            seed_phrase,
190            hd_path: None,
191        };
192        let key = Key::Secret(secret);
193        round_trip(&key);
194    }
195
196    const PUBLIC_KEY: &str = "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC";
197    const MUXED_ACCOUNT: &str =
198        "MA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAAAAAAAAAPCICBKU";
199    const SECRET_KEY: &str = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BH";
200    const SEED_PHRASE: &str =
201        "depth decade power loud smile spatial sign movie judge february rate broccoli";
202
203    #[test]
204    fn parse_public_only_accepts_public_key() {
205        let key = Key::parse_public_only(PUBLIC_KEY).unwrap();
206        assert!(matches!(key, Key::PublicKey(_)));
207    }
208
209    #[test]
210    fn parse_public_only_accepts_muxed_account() {
211        let key = Key::parse_public_only(MUXED_ACCOUNT).unwrap();
212        assert!(matches!(key, Key::MuxedAccount(_)));
213    }
214
215    #[test]
216    fn parse_public_only_rejects_secret_key() {
217        let err = Key::parse_public_only(SECRET_KEY).unwrap_err();
218        assert!(matches!(err, Error::PublicKeyExpected));
219    }
220
221    #[test]
222    fn parse_public_only_rejects_seed_phrase() {
223        let err = Key::parse_public_only(SEED_PHRASE).unwrap_err();
224        assert!(matches!(err, Error::PublicKeyExpected));
225    }
226
227    #[test]
228    fn parse_public_only_rejects_ledger() {
229        let err = Key::parse_public_only("ledger").unwrap_err();
230        assert!(matches!(err, Error::Parse));
231    }
232
233    #[test]
234    fn parse_public_only_rejects_secure_store() {
235        let err = Key::parse_public_only("secure_store:org.stellar.cli-alice").unwrap_err();
236        assert!(matches!(err, Error::PublicKeyExpected));
237    }
238
239    #[test]
240    fn parse_public_only_rejects_garbage() {
241        let err = Key::parse_public_only("not-a-key").unwrap_err();
242        assert!(matches!(err, Error::Parse));
243    }
244}