use anyhow::{Context, Result, anyhow, bail};
use bip39::{Language, Mnemonic};
use inquire::{InquireError, Password, PasswordDisplayMode, validator::Validation};
use std::io::{IsTerminal, Read, Write, stderr, stdin, stdout};
use thiserror::Error;
#[derive(Error, Debug)]
enum MnemonicError {
#[error("Mnemonic must have 12 words, but it has {0}.")]
BadWordCount(usize),
#[error("Mnemonic contains an unknown word (word #{0}).")]
UnknownWord(usize),
#[error("{0}")]
Other(String),
}
impl From<bip39::Error> for MnemonicError {
fn from(error: bip39::Error) -> Self {
match error {
bip39::Error::BadWordCount(count) => MnemonicError::BadWordCount(count),
bip39::Error::UnknownWord(index) => MnemonicError::UnknownWord(index + 1), _ => MnemonicError::Other(error.to_string()),
}
}
}
fn parse_mnemonic(input: &str) -> Result<Mnemonic, MnemonicError> {
let normalized = input.trim().to_lowercase();
Mnemonic::parse_in(Language::English, normalized).map_err(Into::into)
}
fn mnemonic_validator(input: &str) -> Result<Validation, Box<dyn std::error::Error + Send + Sync>> {
match parse_mnemonic(input) {
Ok(_) => Ok(Validation::Valid),
Err(e) => Ok(Validation::Invalid(e.to_string().into())),
}
}
pub fn get_mnemonic() -> Result<Mnemonic> {
let mnemonic_str = if !stdin().is_terminal() {
if stderr().is_terminal() {
eprintln!("Reading mnemonic from stdin...");
}
let mut buffer = String::new();
stdin()
.read_to_string(&mut buffer)
.context("Failed to read mnemonic from stdin")?;
if buffer.trim().is_empty() {
bail!("No mnemonic found on stdin");
}
buffer
} else {
let clear_prompt = || {
let _ = write!(stdout(), "\x1b[2K\x1b[1A\x1b[2K");
};
match Password::new("Enter your 12-word Seedvault mnemonic phrase:")
.with_display_mode(PasswordDisplayMode::Full)
.without_confirmation()
.with_validator(mnemonic_validator)
.with_help_message("Paste or type the phrase, separated by spaces.")
.prompt()
{
Ok(s) => s,
Err(InquireError::OperationInterrupted) => {
clear_prompt();
std::process::exit(0);
}
Err(InquireError::OperationCanceled) => {
std::process::exit(0);
}
Err(e) => {
clear_prompt();
return Err(anyhow::anyhow!(
"Failed to read mnemonic from interactive prompt: {}",
e
));
}
}
};
parse_mnemonic(&mnemonic_str).map_err(|e| anyhow!("Invalid mnemonic phrase: {}", e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mnemonic_valid() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let res = parse_mnemonic(phrase);
assert!(res.is_ok());
}
#[test]
fn test_parse_mnemonic_invalid_word_count() {
let phrase = "abandon abandon";
let res = parse_mnemonic(phrase);
assert!(matches!(res, Err(MnemonicError::BadWordCount(2))));
}
#[test]
fn test_parse_mnemonic_unknown_word() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon foo";
let res = parse_mnemonic(phrase);
assert!(matches!(res, Err(MnemonicError::UnknownWord(12))));
}
}