soroban_cli/commands/keys/
public_key.rs1use crate::{
2 commands::config::{address, locator},
3 config::UnresolvedMuxedAccount,
4 signer::ledger,
5};
6
7#[derive(thiserror::Error, Debug)]
8pub enum Error {
9 #[error(transparent)]
10 Address(#[from] address::Error),
11
12 #[error(transparent)]
13 Ledger(#[from] ledger::Error),
14}
15
16#[derive(Debug, clap::Parser, Clone)]
17#[group(skip)]
18pub struct Cmd {
19 #[arg(required_unless_present = "ledger")]
21 pub name: Option<UnresolvedMuxedAccount>,
22
23 #[arg(long)]
26 pub hd_path: Option<u32>,
27
28 #[arg(long, conflicts_with = "name")]
32 pub ledger: bool,
33
34 #[command(flatten)]
35 pub locator: locator::Args,
36}
37
38impl Cmd {
39 pub async fn run(&self) -> Result<(), Error> {
40 println!("{}", self.public_key().await?);
41 Ok(())
42 }
43
44 pub async fn public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
45 if self.ledger {
46 return Ok(ledger::new(self.hd_path.unwrap_or_default())
47 .await?
48 .public_key()
49 .await?);
50 }
51 let name = self
52 .name
53 .as_ref()
54 .expect("clap requires `name` unless --ledger is set");
55 Ok(public_key_from_muxed(
56 name.resolve_muxed_account(&self.locator, self.hd_path)?,
57 ))
58 }
59}
60
61fn public_key_from_muxed(
62 muxed: soroban_sdk::xdr::MuxedAccount,
63) -> stellar_strkey::ed25519::PublicKey {
64 let bytes = match muxed {
65 soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0,
66 soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0,
67 };
68 stellar_strkey::ed25519::PublicKey(bytes)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use clap::Parser;
75
76 const PUBLIC_KEY: &str = "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC";
77
78 #[test]
79 fn ledger_flag_parses_without_name() {
80 let cmd = Cmd::try_parse_from(["address", "--ledger"]).expect("--ledger alone parses");
81 assert!(cmd.ledger);
82 assert!(cmd.name.is_none());
83 assert_eq!(cmd.hd_path, None);
84 }
85
86 #[test]
87 fn ledger_flag_with_hd_path_parses() {
88 let cmd = Cmd::try_parse_from(["address", "--ledger", "--hd-path", "5"]).unwrap();
89 assert!(cmd.ledger);
90 assert_eq!(cmd.hd_path, Some(5));
91 }
92
93 #[test]
94 fn ledger_flag_conflicts_with_name() {
95 let err = Cmd::try_parse_from(["address", PUBLIC_KEY, "--ledger"])
96 .expect_err("--ledger + name must conflict");
97 assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
98 }
99
100 #[test]
101 fn missing_name_without_ledger_is_rejected() {
102 let err = Cmd::try_parse_from(["address"]).expect_err("name is required without --ledger");
103 assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
104 }
105
106 #[test]
107 fn name_without_ledger_parses() {
108 let cmd = Cmd::try_parse_from(["address", PUBLIC_KEY]).unwrap();
109 assert!(!cmd.ledger);
110 assert!(cmd.name.is_some());
111 }
112}