@@ -7,11 +7,18 @@ use crate::{
77pub 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+
2665impl 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