1use clap::Args;
29use scope::chains::ChainClientFactory;
30use scope::config::{Config, OutputFormat};
31use scope::error::Result;
32use serde::{Deserialize, Serialize};
33use std::fmt;
34use std::path::PathBuf;
35
36#[derive(Debug, Clone, Args)]
38#[command(after_help = "\x1b[1mExamples:\x1b[0m
39 scope interactive
40 scope shell
41 scope interactive --no-banner")]
42pub struct InteractiveArgs {
43 #[arg(long)]
45 pub no_banner: bool,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SessionContext {
51 pub chain: String,
53
54 pub format: OutputFormat,
56
57 pub last_address: Option<String>,
59
60 pub last_tx: Option<String>,
62
63 pub include_tokens: bool,
65
66 pub include_txs: bool,
68
69 pub trace: bool,
71
72 pub decode: bool,
74
75 pub limit: u32,
77}
78
79impl SessionContext {
80 pub fn is_auto_chain(&self) -> bool {
82 self.chain == "auto"
83 }
84}
85
86impl Default for SessionContext {
87 fn default() -> Self {
88 Self {
89 chain: "auto".to_string(),
90 format: OutputFormat::Table,
91 last_address: None,
92 last_tx: None,
93 include_tokens: false,
94 include_txs: false,
95 trace: false,
96 decode: false,
97 limit: 100,
98 }
99 }
100}
101
102impl fmt::Display for SessionContext {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 writeln!(f, "Current Context:")?;
105 if self.is_auto_chain() {
106 writeln!(f, " Chain: auto (inferred from input)")?;
107 } else {
108 writeln!(f, " Chain: {} (pinned)", self.chain)?;
109 }
110 writeln!(f, " Format: {:?}", self.format)?;
111 writeln!(f, " Include Tokens: {}", self.include_tokens)?;
112 writeln!(f, " Include TXs: {}", self.include_txs)?;
113 writeln!(f, " Trace: {}", self.trace)?;
114 writeln!(f, " Decode: {}", self.decode)?;
115 writeln!(f, " Limit: {}", self.limit)?;
116 if let Some(ref addr) = self.last_address {
117 writeln!(f, " Last Address: {}", addr)?;
118 }
119 if let Some(ref tx) = self.last_tx {
120 writeln!(f, " Last TX: {}", tx)?;
121 }
122 Ok(())
123 }
124}
125
126impl SessionContext {
127 fn context_path() -> Option<PathBuf> {
129 scope::paths::data_dir().map(|p| p.join("scope").join("session.yaml"))
130 }
131
132 pub fn load() -> Self {
134 Self::context_path()
135 .map(|path| Self::load_from(&path))
136 .unwrap_or_default()
137 }
138
139 pub fn load_from(path: &std::path::Path) -> Self {
142 std::fs::read_to_string(path)
143 .ok()
144 .and_then(|contents| serde_yaml::from_str(&contents).ok())
145 .unwrap_or_default()
146 }
147
148 pub fn save(&self) -> Result<()> {
150 match Self::context_path() {
151 Some(path) => self.save_to(&path),
152 None => Ok(()),
153 }
154 }
155
156 pub fn save_to(&self, path: &std::path::Path) -> Result<()> {
158 if let Some(parent) = path.parent() {
159 std::fs::create_dir_all(parent)?;
160 }
161 let contents = serde_yaml::to_string(self)
162 .map_err(|e| scope::error::ScopeError::Export(e.to_string()))?;
163 std::fs::write(path, contents)?;
164 Ok(())
165 }
166}
167
168pub async fn run(
174 args: InteractiveArgs,
175 config: &Config,
176 clients: &dyn ChainClientFactory,
177) -> Result<crate::cli::tui::TuiExit> {
178 crate::cli::tui::run(args.no_banner, config, clients).await
179}
180
181#[cfg(test)]
186mod tests {
187 use super::*;
188 #[test]
189 fn test_session_context_default() {
190 let ctx = SessionContext::default();
191 assert_eq!(ctx.chain, "auto");
192 assert_eq!(ctx.format, OutputFormat::Table);
193 assert!(!ctx.include_tokens);
194 assert!(!ctx.include_txs);
195 assert!(!ctx.trace);
196 assert!(!ctx.decode);
197 assert_eq!(ctx.limit, 100);
198 assert!(ctx.last_address.is_none());
199 assert!(ctx.last_tx.is_none());
200 }
201 #[test]
202 fn test_session_context_display() {
203 let ctx = SessionContext::default();
204 let display = format!("{}", ctx);
205 assert!(display.contains("auto"));
206 assert!(display.contains("Table"));
207 }
208 #[test]
209 fn test_interactive_args_default() {
210 let args = InteractiveArgs { no_banner: false };
211 assert!(!args.no_banner);
212 }
213 #[test]
214 fn test_session_context_serialization() {
215 let ctx = SessionContext {
216 chain: "polygon".to_string(),
217 format: OutputFormat::Json,
218 last_address: Some("0xabc".to_string()),
219 last_tx: Some("0xdef".to_string()),
220 include_tokens: true,
221 include_txs: true,
222 trace: true,
223 decode: true,
224 limit: 50,
225 };
226
227 let yaml = serde_yaml::to_string(&ctx).unwrap();
228 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
229 assert_eq!(deserialized.chain, "polygon");
230 assert!(!deserialized.is_auto_chain());
231 assert_eq!(deserialized.format, OutputFormat::Json);
232 assert_eq!(deserialized.last_address.as_deref(), Some("0xabc"));
233 assert_eq!(deserialized.last_tx.as_deref(), Some("0xdef"));
234 assert!(deserialized.include_tokens);
235 assert!(deserialized.include_txs);
236 assert!(deserialized.trace);
237 assert!(deserialized.decode);
238 assert_eq!(deserialized.limit, 50);
239 }
240 #[test]
241 fn test_session_context_display_with_address_and_tx() {
242 let ctx = SessionContext {
243 chain: "polygon".to_string(),
244 last_address: Some("0x1234".to_string()),
245 last_tx: Some("0xabcd".to_string()),
246 ..Default::default()
247 };
248 let display = format!("{}", ctx);
249 assert!(display.contains("0x1234"));
250 assert!(display.contains("0xabcd"));
251 assert!(display.contains("(pinned)"));
252 }
253 #[test]
254 fn test_session_context_display_auto_chain() {
255 let ctx = SessionContext::default();
256 let display = format!("{}", ctx);
257 assert!(display.contains("auto"));
258 assert!(display.contains("inferred from input"));
259 }
260 #[test]
261 fn test_session_context_serialization_roundtrip() {
262 let ctx = SessionContext {
263 chain: "solana".to_string(),
264 include_tokens: true,
265 limit: 25,
266 last_address: Some("0xtest".to_string()),
267 ..Default::default()
268 };
269
270 let yaml = serde_yaml::to_string(&ctx).unwrap();
271 let deserialized: SessionContext = serde_yaml::from_str(&yaml).unwrap();
272 assert_eq!(deserialized.chain, "solana");
273 assert!(deserialized.include_tokens);
274 assert_eq!(deserialized.limit, 25);
275 assert_eq!(deserialized.last_address, Some("0xtest".to_string()));
276 }
277 #[test]
278 fn test_session_context_is_auto_chain() {
279 let auto_ctx = SessionContext::default();
280 assert!(auto_ctx.is_auto_chain());
281 let pinned_ctx = SessionContext {
282 chain: "ethereum".to_string(),
283 ..Default::default()
284 };
285 assert!(!pinned_ctx.is_auto_chain());
286 }
287
288 #[test]
289 fn test_session_context_save_to_and_load_from() {
290 let dir = tempfile::tempdir().unwrap();
292 let path = dir.path().join("nested").join("session.yaml");
293 let ctx = SessionContext {
294 chain: "solana".into(),
295 include_txs: true,
296 last_address: Some("addr".into()),
297 ..Default::default()
298 };
299 ctx.save_to(&path).unwrap();
300 let back = SessionContext::load_from(&path);
301 assert_eq!(back.chain, "solana");
302 assert!(back.include_txs);
303 assert_eq!(back.last_address.as_deref(), Some("addr"));
304 }
305
306 #[test]
307 fn test_session_save_and_load_use_the_test_sandbox() {
308 let path = SessionContext::context_path().unwrap();
312 assert!(path.starts_with(std::env::temp_dir()), "{}", path.display());
313 let ctx = SessionContext {
314 chain: "base".into(),
315 limit: 7,
316 ..Default::default()
317 };
318 ctx.save().unwrap();
319 let back = SessionContext::load();
320 assert_eq!(back.chain, "base");
321 assert_eq!(back.limit, 7);
322 }
323
324 #[test]
325 fn test_session_context_load_from_missing_or_corrupt_is_default() {
326 let dir = tempfile::tempdir().unwrap();
327 assert!(SessionContext::load_from(&dir.path().join("none.yaml")).is_auto_chain());
328 let bad = dir.path().join("bad.yaml");
329 std::fs::write(&bad, "chain: [unclosed").unwrap();
330 assert!(SessionContext::load_from(&bad).is_auto_chain());
331 }
332}