1use inquire::{Confirm, CustomType, Select, Text};
2use miette::{bail, miette, Result};
3use rand::Rng;
4use serde::de::IntoDeserializer;
5use serde_with::{base64::Standard, formats::Padded, DeserializeAs, SerializeAs};
6use std::{
7 fmt::Display,
8 fs::File,
9 io::{empty, stdout, IsTerminal, Write},
10 path::PathBuf,
11 str::FromStr,
12};
13
14use crate::{
15 settings::{KeyType, PubkeyConfig},
16 ExistingKeyPath, NetworkBuilder, NodeBuilder, RNGSeed, Settings, SettingsBuilder,
17};
18
19use super::InitArgs;
20
21#[derive(Debug)]
23pub enum OutputMode {
24 StdOut,
26 File {
28 path: PathBuf,
30 },
31}
32
33#[derive(Debug)]
34enum PubkeyConfigOption {
35 GenerateFromSeed,
36 FromFile,
37}
38
39#[derive(Debug)]
41pub enum KeyArg {
42 File {
44 path: Option<PathBuf>,
46 },
47 Seed {
49 seed: Option<String>,
51 },
52}
53
54#[derive(Debug, Clone)]
55struct PubkeySeed([u8; 32]);
56
57impl Display for PubkeySeed {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 serde_with::base64::Base64::<Standard, Padded>::serialize_as(&self.0, f)
60 }
61}
62
63impl FromStr for PubkeySeed {
64 type Err = serde::de::value::Error;
65
66 fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
67 Ok(Self(
68 serde_with::base64::Base64::<Standard, Padded>::deserialize_as(s.into_deserializer())?,
69 ))
70 }
71}
72
73impl Display for PubkeyConfigOption {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 PubkeyConfigOption::GenerateFromSeed => write!(f, "Generate from seed"),
77 PubkeyConfigOption::FromFile => write!(f, "From file"),
78 }
79 }
80}
81
82pub fn handle_init_command(init_args: InitArgs) -> Result<()> {
84 let output_path = init_args.output_path.clone().unwrap_or_else(Settings::path);
85 let output_mode = if init_args.dry_run {
86 OutputMode::StdOut
87 } else {
88 OutputMode::File {
89 path: output_path.clone(),
90 }
91 };
92
93 let key_arg = init_args
94 .key_file
95 .map(|key_file| KeyArg::File { path: key_file })
96 .or_else(|| {
97 init_args
98 .key_seed
99 .map(|key_seed| KeyArg::Seed { seed: key_seed })
100 });
101
102 let no_input = init_args.no_input || !stdout().is_terminal();
105
106 let mut settings_builder = SettingsBuilder::default();
107 let mut node_builder = NodeBuilder::default();
108 let mut network_builder = NetworkBuilder::default();
109
110 let mut writer = handle_quiet(init_args.quiet)?;
111 let key_type = handle_key_type(init_args.key_type, no_input, &mut writer)?;
112 let keypair_config = handle_key(key_arg, key_type, output_path, no_input, &mut writer)?;
113
114 let network = network_builder
115 .keypair_config(keypair_config)
116 .build()
117 .expect("to build network");
118
119 let node = node_builder
120 .network(network)
121 .build()
122 .expect("to build node");
123
124 let settings = settings_builder
125 .node(node)
126 .build()
127 .expect("to builder settings");
128
129 let settings_toml = toml::to_string_pretty(&settings).expect("to serialize settings");
130
131 handle_output_mode(output_mode, no_input, init_args.force, &mut writer)?
132 .write_all(settings_toml.as_bytes())
133 .expect("to write settings file");
134
135 Ok(())
136}
137
138fn handle_quiet(quiet: bool) -> Result<Box<dyn Write>> {
139 if quiet {
140 Ok(Box::new(empty()))
141 } else {
142 Ok(Box::new(stdout()))
143 }
144}
145
146fn handle_output_mode(
147 output_mode: OutputMode,
148 no_input: bool,
149 force: bool,
150 writer: &mut Box<dyn Write>,
151) -> Result<Box<dyn Write>> {
152 match output_mode {
153 OutputMode::StdOut => Ok(Box::new(stdout())),
154 OutputMode::File { path } if force => {
155 if let Some(parent) = path.parent() {
156 std::fs::create_dir_all(parent).expect("to create parent directory");
157 }
158
159 let settings_file = File::options()
160 .read(true)
161 .write(true)
162 .create(true)
163 .truncate(true)
164 .open(&path)
165 .expect("to open settings file");
166
167 writeln!(writer, "Writing settings to {:?}", path).expect("to write");
168
169 Ok(Box::new(settings_file))
170 }
171 OutputMode::File { path } => {
172 if let Some(parent) = path.parent() {
173 std::fs::create_dir_all(parent).expect("to create parent directory");
174 }
175
176 let settings_file = File::options()
177 .read(true)
178 .write(true)
179 .create_new(true)
180 .open(&path);
181
182 let settings_file = match settings_file {
186 Ok(file) => file,
187 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
188 if no_input {
189 bail!("Aborting... settings file already exists at {:?}. Pass `--force` to overwrite it", path);
190 }
191
192 let should_overwrite = Confirm::new(&format!(
193 "Settings file already exists at {:?}, overwrite?",
194 path
195 ))
196 .with_default(false)
197 .prompt()
198 .map_err(|e| miette!(e))?;
199
200 if !should_overwrite {
201 bail!("Aborting... not overwriting existing settings file");
202 }
203
204 File::options()
205 .read(true)
206 .write(true)
207 .create_new(false)
208 .open(&path)
209 .expect("to open settings file")
210 }
211 err => err.expect("to open settings file"),
212 };
213
214 writeln!(writer, "Writing settings to {:?}", path).expect("to write");
215
216 Ok(Box::new(settings_file))
217 }
218 }
219}
220
221fn handle_key_type(
222 key_type: Option<KeyType>,
223 no_input: bool,
224 _writer: &mut Box<dyn Write>,
225) -> Result<KeyType> {
226 match key_type {
227 Some(key_type) => Ok(key_type),
228 None => {
229 if no_input {
230 bail!("Aborting... cannot prompt for key type in non-interactive mode. Pass `--key-type <KEY_TYPE>` to set it.");
231 }
232
233 let options = vec![KeyType::Ed25519, KeyType::Secp256k1];
234
235 let key_type = Select::new("Select key type", options)
236 .prompt()
237 .map_err(|e| miette!(e))?;
238
239 Ok(key_type)
240 }
241 }
242}
243
244fn handle_key(
245 key_arg: Option<KeyArg>,
246 key_type: KeyType,
247 output_path: PathBuf,
248 no_input: bool,
249 writer: &mut Box<dyn Write>,
250) -> Result<PubkeyConfig> {
251 let config = match key_arg {
252 None => {
253 if no_input {
254 bail!("Aborting... cannot prompt for key in non-interactive mode. Pass `--key-file <KEY_FILE>` or `--key-seed [<KEY_SEED>]` to configure the key.");
255 }
256
257 let options = vec![
258 PubkeyConfigOption::GenerateFromSeed,
259 PubkeyConfigOption::FromFile,
260 ];
261
262 let pubkey_config_choice =
263 Select::new("How would you like to configure the key?", options)
264 .prompt()
265 .map_err(|e| miette!(e))?;
266
267 match pubkey_config_choice {
268 PubkeyConfigOption::GenerateFromSeed => {
269 let seed = CustomType::<PubkeySeed>::new("Enter the seed for the key")
270 .with_default(PubkeySeed(rand::thread_rng().gen::<[u8; 32]>()))
271 .with_default_value_formatter(&|_| "random".to_string())
272 .with_error_message("Please type a base64 encoding of 32 bytes")
273 .with_help_message("Base64 encoded 32 bytes")
274 .prompt()
275 .map_err(|e| miette!(e))?;
276
277 PubkeyConfig::GenerateFromSeed(RNGSeed::new(key_type, seed.0))
278 }
279 PubkeyConfigOption::FromFile => {
280 let default_path = if let Some(parent) = output_path.parent() {
281 parent.join("homestar.pem")
282 } else {
283 Settings::path().join("homestar.pem")
284 };
285
286 let path = Text::new("Enter the path for the key")
287 .with_default(&default_path.display().to_string())
288 .prompt()
289 .map_err(|e| miette!(e))?
290 .into();
291
292 generate_key_file(&path, &key_type, writer)?;
293
294 PubkeyConfig::Existing(ExistingKeyPath::new(key_type, path))
295 }
296 }
297 }
298 Some(KeyArg::File { path }) => {
299 let path = path.unwrap_or_else(|| {
300 if let Some(parent) = output_path.parent() {
301 parent.join("homestar.pem")
302 } else {
303 Settings::path().join("homestar.pem")
304 }
305 });
306
307 generate_key_file(&path, &key_type, writer)?;
308
309 PubkeyConfig::Existing(ExistingKeyPath::new(key_type, path))
310 }
311 Some(KeyArg::Seed { seed: None }) => {
312 let seed = rand::thread_rng().gen::<[u8; 32]>();
313
314 PubkeyConfig::GenerateFromSeed(RNGSeed::new(key_type, seed))
315 }
316 Some(KeyArg::Seed { seed: Some(seed) }) => {
317 let Ok(seed) = PubkeySeed::from_str(&seed) else {
318 bail!("Invalid seed: expected a base64 encoding of 32 bytes")
319 };
320
321 PubkeyConfig::GenerateFromSeed(RNGSeed::new(key_type, seed.0))
322 }
323 };
324
325 config
326 .keypair()
327 .map_err(|e| miette!(format!("Failed to load key: {}", e)))?;
328
329 Ok(config)
330}
331
332fn generate_key_file(
333 path: &PathBuf,
334 key_type: &KeyType,
335 writer: &mut Box<dyn Write>,
336) -> Result<()> {
337 if let Some(parent) = path.parent() {
338 std::fs::create_dir_all(parent).expect("to create parent directory");
339 }
340
341 let key_file = File::options()
342 .read(true)
343 .write(true)
344 .create_new(true)
345 .open(path);
346
347 match key_file {
348 Ok(mut file) => {
350 let key = match *key_type {
351 KeyType::Ed25519 => ed25519_compact::KeyPair::generate().sk.to_pem(),
352 KeyType::Secp256k1 => {
353 std::fs::remove_file(path).expect("to delete key file");
354
355 bail!("Aborting... generating secp256k1 keys is not yet supported, please provide an existing key file, or choose another key type.")
356 }
357 };
358
359 file.write_all(key.as_bytes())
360 .expect("to write to key file");
361
362 writeln!(writer, "Writing key file to {:?}", path).expect("to write");
363 }
364 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
366 writeln!(writer, "Using existing key file {:?}", path).expect("to write");
367 }
368 err => {
369 err.expect("to open key file");
370 }
371 };
372
373 Ok(())
374}