Skip to main content

soroban_cli/config/
address.rs

1use std::{
2    fmt::{self, Display, Formatter},
3    str::FromStr,
4};
5
6use crate::{signer, xdr};
7
8use super::{key, locator, secret, utils};
9
10/// Address can be either a public key or eventually an alias of a address.
11#[derive(Clone, Debug)]
12pub enum UnresolvedMuxedAccount {
13    Resolved(xdr::MuxedAccount),
14    AliasOrSecret(String),
15}
16
17impl Default for UnresolvedMuxedAccount {
18    fn default() -> Self {
19        UnresolvedMuxedAccount::AliasOrSecret(String::default())
20    }
21}
22
23impl Display for UnresolvedMuxedAccount {
24    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
25        match self {
26            UnresolvedMuxedAccount::Resolved(muxed_account) => write!(f, "{muxed_account}"),
27            UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => {
28                write!(f, "{alias_or_secret}")
29            }
30        }
31    }
32}
33
34#[derive(thiserror::Error, Debug)]
35pub enum Error {
36    #[error(transparent)]
37    Locator(#[from] locator::Error),
38    #[error(transparent)]
39    Secret(#[from] secret::Error),
40    #[error(transparent)]
41    Signer(#[from] signer::Error),
42    #[error(transparent)]
43    Key(#[from] key::Error),
44    #[error("Address cannot be used to sign {0}")]
45    CannotSign(xdr::MuxedAccount),
46    #[error("Invalid key name: {0}\n only alphanumeric characters, underscores (_), and hyphens (-) are allowed.")]
47    InvalidKeyNameCharacters(String),
48    #[error("Invalid key name: {0}\n keys cannot exceed 250 characters")]
49    InvalidKeyNameLength(String),
50    #[error(transparent)]
51    Name(#[from] utils::Error),
52}
53
54impl FromStr for UnresolvedMuxedAccount {
55    type Err = Error;
56
57    fn from_str(value: &str) -> Result<Self, Self::Err> {
58        Ok(xdr::MuxedAccount::from_str(value).map_or_else(
59            |_| UnresolvedMuxedAccount::AliasOrSecret(value.to_string()),
60            UnresolvedMuxedAccount::Resolved,
61        ))
62    }
63}
64
65impl UnresolvedMuxedAccount {
66    pub fn resolve_muxed_account(
67        &self,
68        locator: &locator::Args,
69        hd_path: Option<u32>,
70    ) -> Result<xdr::MuxedAccount, Error> {
71        match self {
72            UnresolvedMuxedAccount::Resolved(muxed_account) => Ok(muxed_account.clone()),
73            UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => Ok(locator
74                .read_key_with_secure_store_cache(alias_or_secret, hd_path)?
75                .muxed_account(hd_path)?),
76        }
77    }
78
79    pub fn resolve_secret(
80        &self,
81        locator: &locator::Args,
82        hd_path: Option<u32>,
83    ) -> Result<secret::Secret, Error> {
84        match &self {
85            // A literal public key has no secret on its own, but a stored
86            // identity may hold the matching key. Scan identities by public key
87            // so `G...` signs like its alias would; fall back to `CannotSign`
88            // when nothing matches. Muxed accounts (`M...`) aren't signable
89            // end-to-end yet (see the `todo!` in `sign_soroban_authorizations`),
90            // so they keep returning `CannotSign`.
91            UnresolvedMuxedAccount::Resolved(muxed_account) => {
92                let xdr::MuxedAccount::Ed25519(xdr::Uint256(key)) = muxed_account else {
93                    return Err(Error::CannotSign(muxed_account.clone()));
94                };
95                let target = stellar_strkey::ed25519::PublicKey(*key);
96                locator
97                    .secret_by_public_key(&target, hd_path)?
98                    .ok_or_else(|| Error::CannotSign(muxed_account.clone()))
99            }
100            UnresolvedMuxedAccount::AliasOrSecret(alias_or_secret) => {
101                Ok(locator.read_key(alias_or_secret)?.try_into()?)
102            }
103        }
104    }
105}
106
107#[derive(Clone, Debug)]
108pub struct KeyName(pub String);
109
110impl std::ops::Deref for KeyName {
111    type Target = str;
112    fn deref(&self) -> &Self::Target {
113        &self.0
114    }
115}
116
117impl std::str::FromStr for KeyName {
118    type Err = Error;
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        utils::validate_name(s).map_err(|e| match e {
121            utils::Error::InvalidNameLength(s) => Error::InvalidKeyNameLength(s),
122            utils::Error::InvalidNameCharacters(s) => Error::InvalidKeyNameCharacters(s),
123        })?;
124        Ok(KeyName(s.to_string()))
125    }
126}
127
128impl Display for KeyName {
129    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
130        write!(f, "{}", self.0)
131    }
132}
133
134pub fn validate_name(s: &str) -> Result<(), Error> {
135    Ok(utils::validate_name(s)?)
136}
137
138#[derive(Clone, Debug)]
139pub struct NetworkName(String);
140
141impl std::ops::Deref for NetworkName {
142    type Target = str;
143    fn deref(&self) -> &Self::Target {
144        &self.0
145    }
146}
147
148impl std::str::FromStr for NetworkName {
149    type Err = Error;
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        validate_name(s)?;
152        Ok(NetworkName(s.to_string()))
153    }
154}
155
156impl Display for NetworkName {
157    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
158        write!(f, "{}", self.0)
159    }
160}
161
162#[derive(Clone, Debug)]
163pub struct AliasName(String);
164
165impl std::ops::Deref for AliasName {
166    type Target = str;
167    fn deref(&self) -> &Self::Target {
168        &self.0
169    }
170}
171
172impl std::str::FromStr for AliasName {
173    type Err = Error;
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        validate_name(s)?;
176        Ok(AliasName(s.to_string()))
177    }
178}
179
180impl Display for AliasName {
181    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
182        write!(f, "{}", self.0)
183    }
184}
185
186#[derive(Clone, Debug)]
187pub struct ContractName(String);
188
189impl std::ops::Deref for ContractName {
190    type Target = str;
191    fn deref(&self) -> &Self::Target {
192        &self.0
193    }
194}
195
196impl std::str::FromStr for ContractName {
197    type Err = Error;
198    fn from_str(s: &str) -> Result<Self, Self::Err> {
199        validate_name(s)?;
200        Ok(ContractName(s.to_string()))
201    }
202}
203
204impl Display for ContractName {
205    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
206        write!(f, "{}", self.0)
207    }
208}
209
210impl AsRef<std::path::Path> for ContractName {
211    fn as_ref(&self) -> &std::path::Path {
212        std::path::Path::new(&self.0)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::config::secret::Secret;
220
221    const TEST_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ";
222    const TEST_SECRET_KEY: &str = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BH";
223    const OTHER_PUBLIC_KEY: &str = "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC";
224
225    fn locator_with_identity() -> (tempfile::TempDir, locator::Args) {
226        let dir = tempfile::tempdir().unwrap();
227        let locator = locator::Args {
228            config_dir: Some(dir.path().to_path_buf()),
229        };
230        let secret = Secret::SecretKey {
231            secret_key: TEST_SECRET_KEY.to_string(),
232        };
233        locator.write_identity("alice", &secret).unwrap();
234        (dir, locator)
235    }
236
237    #[test]
238    fn resolve_secret_matches_public_key_to_stored_identity() {
239        let (_dir, locator) = locator_with_identity();
240        let account: UnresolvedMuxedAccount = TEST_PUBLIC_KEY.parse().unwrap();
241        assert!(matches!(account, UnresolvedMuxedAccount::Resolved(_)));
242
243        let secret = account.resolve_secret(&locator, None).unwrap();
244        assert!(matches!(
245            secret,
246            Secret::SecretKey { ref secret_key } if secret_key == TEST_SECRET_KEY
247        ));
248    }
249
250    #[test]
251    fn resolve_secret_errors_when_public_key_has_no_stored_identity() {
252        let (_dir, locator) = locator_with_identity();
253        let account: UnresolvedMuxedAccount = OTHER_PUBLIC_KEY.parse().unwrap();
254
255        assert!(matches!(
256            account.resolve_secret(&locator, None).unwrap_err(),
257            Error::CannotSign(_)
258        ));
259    }
260
261    #[test]
262    fn resolve_secret_rejects_muxed_account_even_with_stored_identity() {
263        let (_dir, locator) = locator_with_identity();
264        // A muxed account (`M...`) wrapping alice's ed25519 key. Even though the
265        // underlying key belongs to a stored identity, muxed accounts aren't
266        // signable end-to-end yet, so resolution must still return `CannotSign`
267        // rather than the stored secret.
268        let pk = stellar_strkey::ed25519::PublicKey::from_string(TEST_PUBLIC_KEY).unwrap();
269        let account = UnresolvedMuxedAccount::Resolved(xdr::MuxedAccount::MuxedEd25519(
270            xdr::MuxedAccountMed25519 {
271                id: 1,
272                ed25519: xdr::Uint256(pk.0),
273            },
274        ));
275
276        assert!(matches!(
277            account.resolve_secret(&locator, None).unwrap_err(),
278            Error::CannotSign(_)
279        ));
280    }
281
282    #[test]
283    fn ledger_shorthand_is_not_recognized() {
284        match "ledger".parse::<UnresolvedMuxedAccount>().unwrap() {
285            UnresolvedMuxedAccount::AliasOrSecret(s) => assert_eq!(s, "ledger"),
286            UnresolvedMuxedAccount::Resolved(m) => panic!("unexpected resolved muxed: {m}"),
287        }
288    }
289
290    #[test]
291    fn ledger_indexed_shorthand_is_not_recognized() {
292        match "ledger:5".parse::<UnresolvedMuxedAccount>().unwrap() {
293            UnresolvedMuxedAccount::AliasOrSecret(s) => assert_eq!(s, "ledger:5"),
294            UnresolvedMuxedAccount::Resolved(m) => panic!("unexpected resolved muxed: {m}"),
295        }
296    }
297
298    #[test]
299    fn network_name_valid() {
300        assert!("my-network".parse::<NetworkName>().is_ok());
301        assert!("my_network_123".parse::<NetworkName>().is_ok());
302        assert!("ledger".parse::<NetworkName>().is_ok());
303    }
304
305    #[test]
306    fn network_name_rejects_path_traversal() {
307        assert!("../evil".parse::<NetworkName>().is_err());
308        assert!("../../etc/passwd".parse::<NetworkName>().is_err());
309        assert!("foo/bar".parse::<NetworkName>().is_err());
310        assert!("foo\\bar".parse::<NetworkName>().is_err());
311    }
312
313    #[test]
314    fn network_name_rejects_too_long() {
315        assert!("a".repeat(251).parse::<NetworkName>().is_err());
316        assert!("a".repeat(250).parse::<NetworkName>().is_ok());
317    }
318
319    #[test]
320    fn alias_name_valid() {
321        assert!("my_alias_123".parse::<AliasName>().is_ok());
322        assert!("ledger".parse::<AliasName>().is_ok());
323    }
324
325    #[test]
326    fn alias_name_rejects_path_traversal() {
327        assert!("../evil".parse::<AliasName>().is_err());
328        assert!("../../etc/passwd".parse::<AliasName>().is_err());
329        assert!("foo/bar".parse::<AliasName>().is_err());
330        assert!("foo\\bar".parse::<AliasName>().is_err());
331    }
332
333    #[test]
334    fn alias_name_rejects_too_long() {
335        assert!("a".repeat(251).parse::<AliasName>().is_err());
336        assert!("a".repeat(250).parse::<AliasName>().is_ok());
337    }
338
339    #[test]
340    fn network_name_rejects_empty() {
341        assert!("".parse::<NetworkName>().is_err());
342    }
343
344    #[test]
345    fn alias_name_rejects_empty() {
346        assert!("".parse::<AliasName>().is_err());
347    }
348
349    #[test]
350    fn contract_name_valid() {
351        assert!("hello-world".parse::<ContractName>().is_ok());
352        assert!("my_contract_123".parse::<ContractName>().is_ok());
353    }
354
355    #[test]
356    fn contract_name_rejects_path_traversal() {
357        assert!("../evil".parse::<ContractName>().is_err());
358        assert!("../../etc/passwd".parse::<ContractName>().is_err());
359        assert!("foo/bar".parse::<ContractName>().is_err());
360        assert!("foo\\bar".parse::<ContractName>().is_err());
361    }
362
363    #[test]
364    fn contract_name_rejects_too_long() {
365        assert!("a".repeat(251).parse::<ContractName>().is_err());
366        assert!("a".repeat(250).parse::<ContractName>().is_ok());
367    }
368
369    #[test]
370    fn contract_name_rejects_empty() {
371        assert!("".parse::<ContractName>().is_err());
372    }
373}