Skip to main content

stacksdapp_shell/
mnemonic.rs

1//! Deployer mnemonic inspection for testnet/mainnet safety checks.
2
3use std::path::Path;
4
5/// Public devnet seeds shipped in scaffold templates — must never be used on testnet/mainnet.
6pub const PUBLIC_DEVNET_MNEMONICS: &[&str] = &[
7    "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw",
8    "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild",
9    "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital",
10    "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high",
11    "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin",
12    "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase",
13    "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy",
14    "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow",
15    "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune",
16    "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform",
17];
18
19const VALID_WORD_COUNTS: [usize; 5] = [12, 15, 18, 21, 24];
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ParsedDeployerMnemonic {
23    pub mnemonic: String,
24    pub line_number: usize,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum MnemonicCheck {
29    /// Placeholder or empty — not configured yet.
30    NotConfigured,
31    Ok,
32    PublicDevnet,
33    InvalidFormat(String),
34}
35
36/// Normalize whitespace for stable comparison.
37pub fn normalize_mnemonic(mnemonic: &str) -> String {
38    mnemonic.split_whitespace().collect::<Vec<_>>().join(" ")
39}
40
41pub fn is_mnemonic_placeholder(mnemonic: &str) -> bool {
42    mnemonic.is_empty() || mnemonic.contains('<') || mnemonic.contains('>')
43}
44
45pub fn is_public_devnet_mnemonic(mnemonic: &str) -> bool {
46    let normalized = normalize_mnemonic(mnemonic);
47    PUBLIC_DEVNET_MNEMONICS
48        .iter()
49        .any(|seed| normalize_mnemonic(seed) == normalized)
50}
51
52/// Word-count + lowercase a-z charset (no BIP39 checksum — Clarinet may accept edge cases).
53pub fn validate_mnemonic_word_format(mnemonic: &str) -> Result<(), String> {
54    let words: Vec<&str> = mnemonic.split_whitespace().collect();
55    let count = words.len();
56    if !VALID_WORD_COUNTS.contains(&count) {
57        return Err(format!(
58            "mnemonic has {count} words (expected 12, 15, 18, 21, or 24)"
59        ));
60    }
61    for word in words {
62        if word.is_empty() || !word.chars().all(|c| c.is_ascii_lowercase()) {
63            return Err(format!(
64                "mnemonic words must be lowercase a-z (invalid token: \"{word}\")"
65            ));
66        }
67    }
68    Ok(())
69}
70
71pub fn parse_deployer_mnemonic(toml_raw: &str) -> Option<ParsedDeployerMnemonic> {
72    let mut in_deployer = false;
73    for (idx, line) in toml_raw.lines().enumerate() {
74        let trimmed = line.trim();
75        if trimmed == "[accounts.deployer]" {
76            in_deployer = true;
77            continue;
78        }
79        if trimmed.starts_with('[') {
80            in_deployer = false;
81        }
82        if in_deployer && trimmed.starts_with("mnemonic") {
83            if let Some((_, val)) = trimmed.split_once('=') {
84                let mnemonic = val.trim().trim_matches('"').to_string();
85                return Some(ParsedDeployerMnemonic {
86                    mnemonic,
87                    line_number: idx + 1,
88                });
89            }
90        }
91    }
92    None
93}
94
95pub fn check_deployer_mnemonic(mnemonic: &str, strict_format: bool) -> MnemonicCheck {
96    if is_mnemonic_placeholder(mnemonic) {
97        return MnemonicCheck::NotConfigured;
98    }
99    if is_public_devnet_mnemonic(mnemonic) {
100        return MnemonicCheck::PublicDevnet;
101    }
102    if strict_format {
103        if let Err(detail) = validate_mnemonic_word_format(mnemonic) {
104            return MnemonicCheck::InvalidFormat(detail);
105        }
106    }
107    MnemonicCheck::Ok
108}
109
110pub fn settings_relative_path(network: &str) -> String {
111    format!("contracts/settings/{}.toml", capitalize(network))
112}
113
114fn capitalize(s: &str) -> String {
115    let mut c = s.chars();
116    match c.next() {
117        None => String::new(),
118        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
119    }
120}
121
122/// Inspect a network settings file on disk (for doctor).
123pub fn inspect_settings_file(
124    network: &str,
125    root: &Path,
126    strict_format: bool,
127) -> Option<(String, MnemonicCheck)> {
128    let path = root.join(settings_relative_path(network));
129    let raw = std::fs::read_to_string(&path).ok()?;
130    let parsed = parse_deployer_mnemonic(&raw)?;
131    let check = check_deployer_mnemonic(&parsed.mnemonic, strict_format);
132    Some((path.display().to_string(), check))
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn detects_public_devnet_deployer_seed() {
141        assert!(is_public_devnet_mnemonic(PUBLIC_DEVNET_MNEMONICS[0]));
142    }
143
144    #[test]
145    fn rejects_placeholder_mnemonic() {
146        assert!(is_mnemonic_placeholder(
147            "<YOUR PRIVATE TESTNET MNEMONIC HERE>"
148        ));
149    }
150
151    #[test]
152    fn validates_word_count_and_charset() {
153        assert!(validate_mnemonic_word_format(PUBLIC_DEVNET_MNEMONICS[0]).is_ok());
154        assert!(validate_mnemonic_word_format("one two three").is_err());
155        assert!(validate_mnemonic_word_format(
156            "One two three four five six seven eight nine ten eleven twelve"
157        )
158        .is_err());
159    }
160
161    #[test]
162    fn parse_finds_line_number() {
163        let toml = r#"[accounts.deployer]
164mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw"
165"#;
166        let parsed = parse_deployer_mnemonic(toml).unwrap();
167        assert_eq!(parsed.line_number, 2);
168        assert!(is_public_devnet_mnemonic(&parsed.mnemonic));
169    }
170}