Skip to content

Commit 7de0eba

Browse files
committed
Add --ledger to stellar keys address.
1 parent 2af8896 commit 7de0eba

4 files changed

Lines changed: 128 additions & 21 deletions

File tree

FULL_HELP_DOCS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,7 @@ Add a new identity (keypair, ledger, OS specific secure store)
11481148

11491149
Given an identity return its address (public key)
11501150

1151-
**Usage:** `stellar keys public-key [OPTIONS] <NAME>`
1151+
**Usage:** `stellar keys public-key [OPTIONS] [NAME]`
11521152

11531153
**Command Alias:** `address`
11541154

@@ -1158,7 +1158,8 @@ Given an identity return its address (public key)
11581158

11591159
###### **Options:**
11601160

1161-
- `--hd-path <HD_PATH>` — If identity is a seed phrase use this hd path, default is 0
1161+
- `--hd-path <HD_PATH>` — If identity is a seed phrase use this hd path, default is 0. With --ledger this is the Ledger account index (default 0)
1162+
- `--ledger` — Derive the address from a connected Ledger hardware wallet at `m/44'/148'/N'`, where `N` defaults to 0 and can be set with `--hd-path`
11621163

11631164
###### **Options (Global):**
11641165

cmd/crates/soroban-test/src/lib.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -289,15 +289,6 @@ impl TestEnv {
289289
&self.temp_dir
290290
}
291291

292-
/// Returns the public key corresponding to the test keys's `hd_path`
293-
pub async fn test_address(&self, hd_path: usize) -> String {
294-
self.cmd::<keys::public_key::Cmd>(&format!("--hd-path={hd_path}"))
295-
.public_key()
296-
.await
297-
.unwrap()
298-
.to_string()
299-
}
300-
301292
/// Returns the private key corresponding to the test keys's `hd_path`
302293
pub fn test_show(&self, hd_path: usize) -> String {
303294
self.cmd::<keys::secret::Cmd>(&format!("--hd-path={hd_path}"))

cmd/soroban-cli/src/commands/keys/fund.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct Cmd {
1717
pub network: network::Args,
1818
/// Address to fund
1919
#[command(flatten)]
20-
pub address: public_key::Cmd,
20+
pub address: public_key::Args,
2121
}
2222

2323
impl Cmd {
@@ -34,3 +34,18 @@ impl Cmd {
3434
Ok(())
3535
}
3636
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use super::*;
41+
use clap::Parser;
42+
43+
const PUBLIC_KEY: &str = "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC";
44+
45+
#[test]
46+
fn fund_does_not_accept_ledger_flag() {
47+
let err = Cmd::try_parse_from(["fund", PUBLIC_KEY, "--ledger"])
48+
.expect_err("`--ledger` belongs to `keys address` only");
49+
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
50+
}
51+
}

cmd/soroban-cli/src/commands/keys/public_key.rs

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ use crate::{
77
pub enum Error {
88
#[error(transparent)]
99
Address(#[from] address::Error),
10+
11+
#[error("--hd-path {0} is out of range for a Ledger account index")]
12+
HdPathOutOfRange(usize),
1013
}
1114

15+
/// The fields shared by every command that resolves an identity to a public
16+
/// key. Embed this with `#[command(flatten)]` to inherit `name`, `hd_path`,
17+
/// and the config locator without picking up `keys address`'s `--ledger`
18+
/// flag.
1219
#[derive(Debug, clap::Parser, Clone)]
1320
#[group(skip)]
14-
pub struct Cmd {
21+
pub struct Args {
1522
/// Name of identity to lookup, default test identity used if not provided
1623
pub name: UnresolvedMuxedAccount,
1724

@@ -23,21 +30,114 @@ pub struct Cmd {
2330
pub locator: locator::Args,
2431
}
2532

33+
impl Args {
34+
pub async fn public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
35+
Ok(public_key_from_muxed(
36+
self.name
37+
.resolve_muxed_account(&self.locator, self.hd_path)
38+
.await?,
39+
))
40+
}
41+
}
42+
43+
#[derive(Debug, clap::Parser, Clone)]
44+
#[group(skip)]
45+
pub struct Cmd {
46+
/// Name of identity to lookup, default test identity used if not provided
47+
#[arg(required_unless_present = "ledger")]
48+
pub name: Option<UnresolvedMuxedAccount>,
49+
50+
/// If identity is a seed phrase use this hd path, default is 0.
51+
/// With --ledger this is the Ledger account index (default 0).
52+
#[arg(long)]
53+
pub hd_path: Option<usize>,
54+
55+
/// Derive the address from a connected Ledger hardware wallet at
56+
/// `m/44'/148'/N'`, where `N` defaults to 0 and can be set with
57+
/// `--hd-path`.
58+
#[arg(long, conflicts_with = "name")]
59+
pub ledger: bool,
60+
61+
#[command(flatten)]
62+
pub locator: locator::Args,
63+
}
64+
2665
impl Cmd {
2766
pub async fn run(&self) -> Result<(), Error> {
2867
println!("{}", self.public_key().await?);
2968
Ok(())
3069
}
3170

3271
pub async fn public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
33-
let muxed = self
72+
if self.ledger {
73+
let raw = self.hd_path.unwrap_or(0);
74+
let index: u32 = raw.try_into().map_err(|_| Error::HdPathOutOfRange(raw))?;
75+
return Ok(public_key_from_muxed(
76+
UnresolvedMuxedAccount::Ledger(index)
77+
.resolve_muxed_account(&self.locator, None)
78+
.await?,
79+
));
80+
}
81+
let name = self
3482
.name
35-
.resolve_muxed_account(&self.locator, self.hd_path)
36-
.await?;
37-
let bytes = match muxed {
38-
soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0,
39-
soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0,
40-
};
41-
Ok(stellar_strkey::ed25519::PublicKey(bytes))
83+
.as_ref()
84+
.expect("clap requires `name` unless --ledger is set");
85+
Ok(public_key_from_muxed(
86+
name.resolve_muxed_account(&self.locator, self.hd_path)
87+
.await?,
88+
))
89+
}
90+
}
91+
92+
fn public_key_from_muxed(
93+
muxed: soroban_sdk::xdr::MuxedAccount,
94+
) -> stellar_strkey::ed25519::PublicKey {
95+
let bytes = match muxed {
96+
soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0,
97+
soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0,
98+
};
99+
stellar_strkey::ed25519::PublicKey(bytes)
100+
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use super::*;
105+
use clap::Parser;
106+
107+
const PUBLIC_KEY: &str = "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC";
108+
109+
#[test]
110+
fn ledger_flag_parses_without_name() {
111+
let cmd = Cmd::try_parse_from(["address", "--ledger"]).expect("--ledger alone parses");
112+
assert!(cmd.ledger);
113+
assert!(cmd.name.is_none());
114+
assert_eq!(cmd.hd_path, None);
115+
}
116+
117+
#[test]
118+
fn ledger_flag_with_hd_path_parses() {
119+
let cmd = Cmd::try_parse_from(["address", "--ledger", "--hd-path", "5"]).unwrap();
120+
assert!(cmd.ledger);
121+
assert_eq!(cmd.hd_path, Some(5));
122+
}
123+
124+
#[test]
125+
fn ledger_flag_conflicts_with_name() {
126+
let err = Cmd::try_parse_from(["address", PUBLIC_KEY, "--ledger"])
127+
.expect_err("--ledger + name must conflict");
128+
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
129+
}
130+
131+
#[test]
132+
fn missing_name_without_ledger_is_rejected() {
133+
let err = Cmd::try_parse_from(["address"]).expect_err("name is required without --ledger");
134+
assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
135+
}
136+
137+
#[test]
138+
fn name_without_ledger_parses() {
139+
let cmd = Cmd::try_parse_from(["address", PUBLIC_KEY]).unwrap();
140+
assert!(!cmd.ledger);
141+
assert!(cmd.name.is_some());
42142
}
43143
}

0 commit comments

Comments
 (0)