1use super::{
2 TouchIdSidecarState, ensure_touch_id_available, existing_keystore_path,
3 remove_touch_id_sidecar, touch_id_sidecar_path, touch_id_sidecar_policy,
4 touch_id_sidecar_state,
5};
6use clap::{Args, Parser};
7use eyre::Result;
8use foundry_cli::json::print_json_success;
9use foundry_common::{sh_println, shell};
10use serde_json::json;
11
12#[cfg(all(target_os = "macos", feature = "touch-id"))]
13use super::{ensure_touch_id_sidecar_available, password_or_prompt};
14#[cfg(all(target_os = "macos", feature = "touch-id"))]
15use alloy_signer_local::PrivateKeySigner;
16
17#[derive(Debug, Args)]
19pub struct TouchIdArgs {
20 #[command(subcommand)]
21 command: TouchIdSubcommands,
22}
23
24impl TouchIdArgs {
25 pub fn run(self) -> Result<()> {
26 self.command.run()
27 }
28}
29
30#[derive(Debug, Parser)]
32enum TouchIdSubcommands {
33 Enroll {
35 #[arg(value_name = "ACCOUNT_NAME")]
37 account_name: String,
38
39 #[arg(long, short)]
41 keystore_dir: Option<String>,
42
43 #[arg(long, env = "CAST_UNSAFE_PASSWORD", value_name = "PASSWORD")]
45 unsafe_password: Option<String>,
46 },
47
48 Status {
50 #[arg(value_name = "ACCOUNT_NAME")]
52 account_name: String,
53
54 #[arg(long, short)]
56 keystore_dir: Option<String>,
57 },
58
59 Remove {
61 #[arg(value_name = "ACCOUNT_NAME")]
63 account_name: String,
64
65 #[arg(long, short)]
67 keystore_dir: Option<String>,
68 },
69}
70
71impl TouchIdSubcommands {
72 fn run(self) -> Result<()> {
73 match self {
74 Self::Enroll { account_name, keystore_dir, unsafe_password } => {
75 enroll(&account_name, keystore_dir, unsafe_password)
76 }
77 Self::Status { account_name, keystore_dir } => status(&account_name, keystore_dir),
78 Self::Remove { account_name, keystore_dir } => remove(&account_name, keystore_dir),
79 }
80 }
81}
82
83fn status(account_name: &str, keystore_dir: Option<String>) -> Result<()> {
84 let keystore_path = existing_keystore_path(account_name, keystore_dir)?;
85 let sidecar = touch_id_sidecar_path(&keystore_path);
86
87 match touch_id_sidecar_state(&sidecar)? {
88 TouchIdSidecarState::Missing => print_status(
89 json!({"account": account_name, "status": "not-enrolled"}),
90 format!("Touch ID is not enrolled for keystore `{account_name}`."),
91 ),
92 TouchIdSidecarState::Recognized => {
93 let policy = touch_id_sidecar_policy(&sidecar)?.as_str();
94 print_status(
95 json!({"account": account_name, "status": "enrolled", "policy": policy}),
96 format!(
97 "Touch ID is enrolled for keystore `{account_name}` with `{policy}` policy."
98 ),
99 )
100 }
101 TouchIdSidecarState::Keystore => print_status(
102 json!({"account": account_name, "status": "conflict"}),
103 format!(
104 "Touch ID status for keystore `{account_name}` is conflicted: {} is an existing keystore.",
105 sidecar.display()
106 ),
107 ),
108 TouchIdSidecarState::Unknown => print_status(
109 json!({"account": account_name, "status": "unknown"}),
110 format!(
111 "Touch ID status for keystore `{account_name}` is unknown: {} is not a recognized Touch ID sidecar.",
112 sidecar.display()
113 ),
114 ),
115 }
116}
117
118fn remove(account_name: &str, keystore_dir: Option<String>) -> Result<()> {
119 let keystore_path = existing_keystore_path(account_name, keystore_dir)?;
120 let removed = remove_touch_id_sidecar(&keystore_path)?;
121 let message = if removed {
122 format!("Touch ID enrollment removed for keystore `{account_name}`.")
123 } else {
124 format!("Touch ID is not enrolled for keystore `{account_name}`.")
125 };
126 print_status(json!({"account": account_name, "removed": removed}), message)
127}
128
129#[cfg(all(target_os = "macos", feature = "touch-id"))]
130fn enroll(
131 account_name: &str,
132 keystore_dir: Option<String>,
133 unsafe_password: Option<String>,
134) -> Result<()> {
135 let keystore_path = existing_keystore_path(account_name, keystore_dir)?;
136 ensure_touch_id_available(true)?;
137
138 let sidecar = touch_id_sidecar_path(&keystore_path);
139 let state = touch_id_sidecar_state(&sidecar)?;
140 ensure_touch_id_sidecar_available(&keystore_path)?;
141 let (reenrolled, policy) = match state {
142 TouchIdSidecarState::Missing => (false, foundry_wallets::touch_id::Policy::default()),
143 TouchIdSidecarState::Recognized => {
144 (true, foundry_wallets::touch_id::policy(&keystore_path)?)
145 }
146 TouchIdSidecarState::Keystore | TouchIdSidecarState::Unknown => {
147 eyre::bail!("Touch ID sidecar state changed during enrollment preflight");
148 }
149 };
150
151 let password = password_or_prompt(unsafe_password, "Enter password: ")?;
152 PrivateKeySigner::decrypt_keystore(&keystore_path, &password)
153 .map_err(|_| eyre::eyre!("Invalid password - Touch ID enrollment cancelled"))?;
154
155 foundry_wallets::touch_id::enroll(&keystore_path, &password, policy).map_err(|error| {
156 let action = if reenrolled { "re-enrollment" } else { "enrollment" };
157 eyre::eyre!("Touch ID {action} failed for keystore `{account_name}`: {error}")
158 })?;
159
160 let message = if reenrolled {
161 format!("Touch ID re-enrolled for keystore `{account_name}`.")
162 } else {
163 format!("Touch ID enrolled for keystore `{account_name}`.")
164 };
165 print_status(
166 json!({"account": account_name, "touch_id": true, "reenrolled": reenrolled}),
167 message,
168 )
169}
170
171#[cfg(not(all(target_os = "macos", feature = "touch-id")))]
172fn enroll(
173 account_name: &str,
174 keystore_dir: Option<String>,
175 _unsafe_password: Option<String>,
176) -> Result<()> {
177 existing_keystore_path(account_name, keystore_dir)?;
178 ensure_touch_id_available(true)
179}
180
181fn print_status(value: serde_json::Value, message: String) -> Result<()> {
182 if shell::is_json() { print_json_success(value) } else { sh_println!("{message}") }
183}