1use futu_auth::{KEYRING_SERVICE, KEYRING_USERNAME_TRADE_PWD, keyring_username_for_trade_pwd};
12
13fn non_empty_trimmed(s: &str) -> Option<String> {
14 let s = s.trim();
15 (!s.is_empty()).then(|| s.to_string())
16}
17
18fn trade_pwd_account_from(
19 explicit: Option<&str>,
20 trade_pwd_account_env: Option<&str>,
21 futu_account_env: Option<&str>,
22) -> Option<String> {
23 explicit
24 .and_then(non_empty_trimmed)
25 .or_else(|| trade_pwd_account_env.and_then(non_empty_trimmed))
26 .or_else(|| futu_account_env.and_then(non_empty_trimmed))
27}
28
29fn read_keyring_password(username: &str) -> Option<String> {
30 if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, username)
31 && let Ok(pwd) = entry.get_password()
32 && !pwd.is_empty()
33 {
34 return Some(pwd);
35 }
36 None
37}
38
39fn trade_password_from_sources(
40 account_hint: Option<&str>,
41 trade_pwd_account_env: Option<&str>,
42 futu_account_env: Option<&str>,
43 env_pwd: Option<&str>,
44 mut read_keyring: impl FnMut(&str) -> Option<String>,
45) -> Result<String, String> {
46 let env_pwd = env_pwd.and_then(non_empty_trimmed);
47 if let Some(account) =
48 trade_pwd_account_from(account_hint, trade_pwd_account_env, futu_account_env)
49 {
50 let scoped_username = keyring_username_for_trade_pwd(&account);
51 if let Some(pwd) = read_keyring(&scoped_username) {
52 return Ok(pwd);
53 }
54 if let Some(pwd) = env_pwd {
55 return Ok(pwd);
56 }
57 if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD) {
58 return Ok(pwd);
59 }
60 return Err(format!(
61 "no trade password configured for account {account}: run `futucli set-trade-pwd \
62 --account {account}`, then start futu-mcp with `--trade-pwd-account {account}` \
63 (or FUTU_TRADE_PWD_ACCOUNT={account}); alternatively set FUTU_TRADE_PWD"
64 ));
65 }
66
67 if let Some(pwd) = read_keyring(KEYRING_USERNAME_TRADE_PWD) {
68 return Ok(pwd);
69 }
70 if let Some(pwd) = env_pwd {
71 return Ok(pwd);
72 }
73 Err(
74 "no trade password configured: run `futucli set-trade-pwd --account <login-account>` \
75 and start futu-mcp with `--trade-pwd-account <login-account>` \
76 (or set FUTU_TRADE_PWD_ACCOUNT); legacy deployments may still use FUTU_TRADE_PWD"
77 .to_string(),
78 )
79}
80
81pub fn get_trade_password_for_account(account_hint: Option<&str>) -> Result<String, String> {
83 let trade_pwd_account_env = std::env::var("FUTU_TRADE_PWD_ACCOUNT").ok();
84 let futu_account_env = std::env::var("FUTU_ACCOUNT").ok();
85 let env_pwd = std::env::var("FUTU_TRADE_PWD").ok();
86 trade_password_from_sources(
87 account_hint,
88 trade_pwd_account_env.as_deref(),
89 futu_account_env.as_deref(),
90 env_pwd.as_deref(),
91 read_keyring_password,
92 )
93}
94
95pub fn get_trade_password_md5_for_account(account_hint: Option<&str>) -> Result<String, String> {
97 let pwd = get_trade_password_for_account(account_hint)?;
98 Ok(md5_hex(&pwd))
99}
100
101fn md5_hex(pwd: &str) -> String {
102 format!("{:x}", md5::compute(pwd.as_bytes()))
103}
104
105#[cfg(test)]
106mod tests;