Skip to main content

soroban_cli/commands/keys/
public_key.rs

1use 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    /// Name of identity to lookup. Required unless `--ledger` is provided.
20    #[arg(required_unless_present = "ledger")]
21    pub name: Option<UnresolvedMuxedAccount>,
22
23    /// If identity is a seed phrase use this hd path, default is 0.
24    /// With --ledger this is the Ledger account index (default 0).
25    #[arg(long)]
26    pub hd_path: Option<u32>,
27
28    /// Derive the address from a connected Ledger hardware wallet at
29    /// `m/44'/148'/N'`, where `N` defaults to 0 and can be set with
30    /// `--hd-path`.
31    #[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}