stacksdapp_shell/
mnemonic.rs1use std::path::Path;
4
5pub 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 NotConfigured,
31 Ok,
32 PublicDevnet,
33 InvalidFormat(String),
34}
35
36pub 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
52pub 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
122pub 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}