@@ -401,6 +401,28 @@ impl Args {
401401 Ok ( self . read_key ( key_or_name) ?. muxed_account ( hd_path) ?)
402402 }
403403
404+ /// Find a stored identity whose public key matches `target`, returning its
405+ /// secret. Each identity is derived at `hd_path` (falling back to its own
406+ /// persisted path when `hd_path` is `None`), so a key looked up by strkey
407+ /// resolves the same way it would by alias under the same `--hd-path`.
408+ /// Best-effort: identities whose public key can't be derived without error
409+ /// (e.g. a disconnected ledger) are skipped rather than failing the lookup.
410+ pub fn secret_by_public_key (
411+ & self ,
412+ target : & stellar_strkey:: ed25519:: PublicKey ,
413+ hd_path : Option < u32 > ,
414+ ) -> Result < Option < Secret > , Error > {
415+ for name in self . list_identities ( ) ? {
416+ let Ok ( Key :: Secret ( secret) ) = self . read_identity ( & name) else {
417+ continue ;
418+ } ;
419+ if secret. public_key ( hd_path) . is_ok_and ( |pk| & pk == target) {
420+ return Ok ( Some ( secret) ) ;
421+ }
422+ }
423+ Ok ( None )
424+ }
425+
404426 pub fn read_network ( & self , name : & str ) -> Result < Network , Error > {
405427 utils:: validate_name ( name) ?;
406428 let res = KeyType :: Network . read_with_global ( name, self ) ;
@@ -1525,4 +1547,76 @@ mod tests {
15251547 assert ! ( matches!( key, Key :: PublicKey ( _) ) ) ;
15261548 }
15271549 }
1550+
1551+ mod secret_by_public_key {
1552+ use super :: super :: * ;
1553+
1554+ const TEST_PUBLIC_KEY : & str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ" ;
1555+ const TEST_SECRET_KEY : & str = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BH" ;
1556+ const OTHER_PUBLIC_KEY : & str = "GAKSH6AD2IPJQELTHIOWDAPYX74YELUOWJLI2L4RIPIPZH6YQIFNUSDC" ;
1557+ const TEST_SEED_PHRASE : & str =
1558+ "depth decade power loud smile spatial sign movie judge february rate broccoli" ;
1559+
1560+ fn locator_with_tempdir ( ) -> ( tempfile:: TempDir , Args ) {
1561+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1562+ let args = Args {
1563+ config_dir : Some ( dir. path ( ) . to_path_buf ( ) ) ,
1564+ } ;
1565+ ( dir, args)
1566+ }
1567+
1568+ #[ test]
1569+ fn returns_secret_for_stored_identity ( ) {
1570+ let ( _dir, locator) = locator_with_tempdir ( ) ;
1571+ let secret = Secret :: SecretKey {
1572+ secret_key : TEST_SECRET_KEY . to_string ( ) ,
1573+ } ;
1574+ locator. write_identity ( "alice" , & secret) . unwrap ( ) ;
1575+
1576+ let target = stellar_strkey:: ed25519:: PublicKey :: from_string ( TEST_PUBLIC_KEY ) . unwrap ( ) ;
1577+ let found = locator. secret_by_public_key ( & target, None ) . unwrap ( ) ;
1578+
1579+ assert ! ( matches!(
1580+ found,
1581+ Some ( Secret :: SecretKey { ref secret_key } ) if secret_key == TEST_SECRET_KEY
1582+ ) ) ;
1583+ }
1584+
1585+ #[ test]
1586+ fn returns_none_for_unknown_public_key ( ) {
1587+ let ( _dir, locator) = locator_with_tempdir ( ) ;
1588+ let secret = Secret :: SecretKey {
1589+ secret_key : TEST_SECRET_KEY . to_string ( ) ,
1590+ } ;
1591+ locator. write_identity ( "alice" , & secret) . unwrap ( ) ;
1592+
1593+ let target = stellar_strkey:: ed25519:: PublicKey :: from_string ( OTHER_PUBLIC_KEY ) . unwrap ( ) ;
1594+ assert ! ( locator
1595+ . secret_by_public_key( & target, None )
1596+ . unwrap( )
1597+ . is_none( ) ) ;
1598+ }
1599+
1600+ #[ test]
1601+ fn matches_identity_at_requested_hd_path ( ) {
1602+ let ( _dir, locator) = locator_with_tempdir ( ) ;
1603+ let secret = Secret :: SeedPhrase {
1604+ seed_phrase : TEST_SEED_PHRASE . to_string ( ) ,
1605+ hd_path : None ,
1606+ } ;
1607+ locator. write_identity ( "alice" , & secret) . unwrap ( ) ;
1608+
1609+ // The account derived at index 5 is only found when the lookup uses
1610+ // the same hd_path; the default (index 0) path must not match it.
1611+ let at_five = secret. public_key ( Some ( 5 ) ) . unwrap ( ) ;
1612+ assert ! ( locator
1613+ . secret_by_public_key( & at_five, Some ( 5 ) )
1614+ . unwrap( )
1615+ . is_some( ) ) ;
1616+ assert ! ( locator
1617+ . secret_by_public_key( & at_five, None )
1618+ . unwrap( )
1619+ . is_none( ) ) ;
1620+ }
1621+ }
15281622}
0 commit comments