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#[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 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 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}