solana_tools_lite_cli/shell/
error.rs1use solana_tools_lite::errors::{
2 AsExitCode, Bip39Error, DeserializeError, ExitCode, GenError, KeypairError, SignError,
3 TransactionParseError, ToolError, VerifyError,
4};
5use std::io;
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum CliError {
10 #[error(transparent)]
11 Core(#[from] ToolError),
12 #[error("--summary-json requires --output (file) to keep signed tx off stdout")]
13 SummaryRequiresOutput,
14 #[error("Fee {fee_lamports} exceeds max-fee limit {max_lamports} lamports")]
15 FeeLimitExceeded {
16 fee_lamports: u128,
17 max_lamports: u64,
18 },
19 #[error("User rejected signing")]
20 UserRejected,
21 #[error("failed to encode summary json: {0}")]
22 SummaryEncode(String),
23 #[error("failed to encode json output: {0}")]
24 PresentationEncode(String),
25 #[error("failed to read stdin: {0}")]
26 StdinRead(String),
27}
28
29impl AsExitCode for CliError {
30 fn as_exit_code(&self) -> i32 {
31 match self {
32 CliError::Core(err) => err.as_exit_code(),
33 CliError::SummaryRequiresOutput | CliError::UserRejected => ExitCode::Usage.as_i32(),
34 CliError::FeeLimitExceeded { .. } => ExitCode::DataErr.as_i32(),
35 CliError::SummaryEncode(_) | CliError::PresentationEncode(_) => {
36 ExitCode::Software.as_i32()
37 }
38 CliError::StdinRead(_) => ExitCode::IoErr.as_i32(),
39 }
40 }
41}
42
43pub fn fail_invalid_input(context: &str, message: &str) -> ! {
45 eprintln!("{}: {}", context, message);
46 std::process::exit(64);
47}
48
49pub fn report_cli_error(context: &str, err: CliError) -> ! {
51 eprintln!("{}: {}", context, format_cli_error(&err));
52 std::process::exit(err.as_exit_code());
53}
54
55fn format_cli_error(err: &CliError) -> String {
56 match err {
57 CliError::Core(core) => format_user_friendly(core),
58 CliError::SummaryRequiresOutput => {
59 "--summary-json requires --output (file) to keep signed tx off stdout".to_string()
60 }
61 CliError::FeeLimitExceeded {
62 fee_lamports,
63 max_lamports,
64 } => format!(
65 "Fee {} exceeds max-fee limit {} lamports",
66 fee_lamports, max_lamports
67 ),
68 CliError::UserRejected => "User rejected signing".to_string(),
69 CliError::SummaryEncode(msg) => format!("failed to encode summary json: {msg}"),
70 CliError::PresentationEncode(msg) => format!("failed to encode json output: {msg}"),
71 CliError::StdinRead(msg) => format!("failed to read stdin: {msg}"),
72 }
73}
74
75fn format_user_friendly(err: &ToolError) -> String {
76 match err {
77 ToolError::Bip39(e) => format_bip39(e),
78 ToolError::Base58(e) => format!("Invalid Base58 encoding: {}", e),
79 ToolError::Sign(e) => format_sign(e),
80 ToolError::Keypair(e) => format_keypair(e),
81 ToolError::Gen(e) => format_gen(e),
82 ToolError::TransactionParse(e) => format_tx_parse(e),
83 ToolError::Deserialize(e) => format_deserialize(e),
84 ToolError::Verify(e) => format_verify(e),
85 ToolError::Io(io_err) => format_io(io_err),
86 ToolError::FileExists { path } => {
87 format!(
88 "Cannot create file '{}': already exists\nHint: Use --force to overwrite",
89 path
90 )
91 }
92 ToolError::InvalidInput(msg) => msg.clone(),
93 ToolError::ConfigurationError(msg) => format!("Configuration error: {}", msg),
94 }
95}
96
97fn format_bip39(e: &Bip39Error) -> String {
98 match e {
99 Bip39Error::InvalidWordCount(got) => {
100 format!(
101 "Invalid mnemonic length: got {} words, expected 12 or 24",
102 got
103 )
104 }
105 Bip39Error::Mnemonic(msg) => {
106 format!(
107 "Mnemonic validation failed: {}\nHint: Check that all words are from the BIP-39 wordlist",
108 msg
109 )
110 }
111 }
112}
113
114fn format_sign(e: &SignError) -> String {
115 match e {
116 SignError::InvalidBase58 => "Invalid Base58 encoding in secret key".to_string(),
117 SignError::InvalidPubkeyFormat => {
118 "Invalid public key format\nHint: Public keys must be valid Base58-encoded Ed25519 keys"
119 .to_string()
120 }
121 SignError::InvalidKeyLength => "Secret key must be exactly 32 bytes".to_string(),
122 SignError::SigningFailed(msg) => {
123 format!(
124 "Failed to sign transaction: {}\nHint: Verify your secret key is valid",
125 msg
126 )
127 }
128 SignError::SignerKeyNotFound => {
129 "Signer public key not found in transaction account keys\nHint: The keypair you're using doesn't match any signer in this transaction".to_string()
130 }
131 SignError::SigningNotRequiredForKey => {
132 "The provided signer is not a required signer for this transaction\nHint: Check that you're using the correct keypair".to_string()
133 }
134 SignError::JsonParse(e) => {
135 format!(
136 "Failed to parse input JSON: {}\nHint: Check that your JSON is valid",
137 e
138 )
139 }
140 }
141}
142
143fn format_keypair(e: &KeypairError) -> String {
144 match e {
145 KeypairError::SeedTooShort(got) => {
146 format!(
147 "Seed is too short: got {} bytes, expected at least 32 bytes",
148 got
149 )
150 }
151 KeypairError::SeedSlice(msg) => {
152 format!("Invalid seed data: {}", msg)
153 }
154 }
155}
156
157fn format_gen(e: &GenError) -> String {
158 match e {
159 GenError::InvalidSeedLength => {
160 "Invalid seed length: expected 64 bytes\nHint: Use a valid BIP-39 mnemonic or provide a 64-byte seed".to_string()
161 }
162 GenError::CryptoError(msg) => {
163 format!(
164 "Cryptographic error: {}\nHint: Check your mnemonic and derivation path",
165 msg
166 )
167 }
168 GenError::InvalidDerivationPath(msg) => {
169 format!(
170 "Invalid derivation path: {}\nHint: Use BIP-44 format like m/44'/501'/0'/0'",
171 msg
172 )
173 }
174 }
175}
176
177fn format_tx_parse(e: &TransactionParseError) -> String {
178 match e {
179 TransactionParseError::InvalidBase64(msg) => {
180 format!(
181 "Invalid Base64 encoding in transaction: {}\nHint: Check that the transaction is properly Base64-encoded",
182 msg
183 )
184 }
185 TransactionParseError::InvalidBase58(msg) => {
186 format!(
187 "Invalid Base58 encoding in transaction: {}\nHint: Check that the transaction is properly Base58-encoded",
188 msg
189 )
190 }
191 TransactionParseError::InvalidInstructionData(msg) => {
192 format!("Invalid instruction data: {}", msg)
193 }
194 TransactionParseError::InvalidPubkeyFormat(msg) => {
195 format!("Invalid public key in transaction: {}", msg)
196 }
197 TransactionParseError::InvalidSignatureLength(len) => {
198 format!("Invalid signature length: expected 64 bytes, got {}", len)
199 }
200 TransactionParseError::InvalidPubkeyLength(len) => {
201 format!("Invalid public key length: expected 32 bytes, got {}", len)
202 }
203 TransactionParseError::InvalidSignatureFormat(msg) => {
204 format!("Invalid signature format: {}", msg)
205 }
206 TransactionParseError::InvalidBlockhashLength(len) => {
207 format!("Invalid blockhash length: expected 32 bytes, got {}", len)
208 }
209 TransactionParseError::InvalidBlockhashFormat(msg) => {
210 format!("Invalid blockhash format: {}", msg)
211 }
212 TransactionParseError::InvalidFormat(msg) => {
213 format!(
214 "Invalid transaction format: {}\nHint: Ensure the transaction is in JSON, Base64, or Base58 format",
215 msg
216 )
217 }
218 TransactionParseError::Serialization(msg) => {
219 format!("Failed to serialize transaction: {}", msg)
220 }
221 }
222}
223
224fn format_deserialize(e: &DeserializeError) -> String {
225 match e {
226 DeserializeError::Deserialization(msg) => {
227 format!(
228 "Failed to deserialize transaction: {}\nHint: Check that the input is a valid Solana transaction",
229 msg
230 )
231 }
232 }
233}
234
235fn format_verify(e: &VerifyError) -> String {
236 match e {
237 VerifyError::Base58Decode(e) => format!("Invalid Base58 encoding: {}", e),
238 VerifyError::InvalidSignatureLength(len) => {
239 format!("Invalid signature length: expected 64 bytes, got {}", len)
240 }
241 VerifyError::InvalidPubkeyLength(len) => {
242 format!("Invalid public key length: expected 32 bytes, got {}", len)
243 }
244 VerifyError::InvalidSignatureFormat => {
245 "Invalid signature format\nHint: Signatures must be 64-byte Ed25519 signatures".to_string()
246 }
247 VerifyError::InvalidPubkeyFormat => {
248 "Invalid public key format\nHint: Public keys must be 32-byte Ed25519 public keys"
249 .to_string()
250 }
251 VerifyError::VerificationFailed => {
252 "Signature verification failed\nHint: The signature does not match the message and public key"
253 .to_string()
254 }
255 }
256}
257
258
259fn format_io(err: &solana_tools_lite::errors::IoError) -> String {
260 let hint = match err.kind() {
264 io::ErrorKind::NotFound => "\nHint: Check that the file path is correct",
265 io::ErrorKind::PermissionDenied => {
266 "\nHint: Ensure you have read/write permissions for this file"
267 }
268 io::ErrorKind::AlreadyExists => "\nHint: Use --force to overwrite existing files",
269 _ => "",
270 };
271
272 format!("{}{}", err, hint)
274}