degenbot_cli_core/context.rs
1//! The driver-domain context a command executes against (ADR-051 D8).
2//!
3//! The database path / chain id / node URIs are resolved through the
4//! `degenbot-config` resolvers — the SAME cascades the Python console reads —
5//! over an injectable [`EnvVars`] seam (never `std::env`), with the CLI override
6//! values the argv facade threaded in.
7
8use std::path::PathBuf;
9
10use degenbot_config::{
11 resolve_chain_id, resolve_database_path, resolve_node_http_uri, resolve_node_uris, EnvVars,
12 Resolved, ResolvedNodeUris,
13};
14
15/// The resolved inputs a console command runs against.
16pub struct CliContext<'a> {
17 env: &'a dyn EnvVars,
18 database: Option<String>,
19 chain_id: Option<String>,
20 node_http: Option<String>,
21 node_ws: Option<String>,
22 config: Option<String>,
23}
24
25impl<'a> CliContext<'a> {
26 /// A context over `env` with no CLI overrides (env + defaults only).
27 #[must_use]
28 pub fn new(env: &'a dyn EnvVars) -> Self {
29 Self {
30 env,
31 database: None,
32 chain_id: None,
33 node_http: None,
34 node_ws: None,
35 config: None,
36 }
37 }
38
39 /// Set the `--database` override (highest-precedence layer).
40 #[must_use]
41 pub fn with_database(mut self, database: impl Into<String>) -> Self {
42 self.database = Some(database.into());
43 self
44 }
45
46 /// Set the `--chain-id` override.
47 #[must_use]
48 pub fn with_chain_id(mut self, chain_id: impl Into<String>) -> Self {
49 self.chain_id = Some(chain_id.into());
50 self
51 }
52
53 /// Set the `--node-http` override.
54 #[must_use]
55 pub fn with_node_http(mut self, uri: impl Into<String>) -> Self {
56 self.node_http = Some(uri.into());
57 self
58 }
59
60 /// Set the `--node-ws` override.
61 #[must_use]
62 pub fn with_node_ws(mut self, uri: impl Into<String>) -> Self {
63 self.node_ws = Some(uri.into());
64 self
65 }
66
67 /// Set the `--config` override (the typed config file the strategy
68 /// verbs read and write).
69 #[must_use]
70 pub fn with_config(mut self, path: impl Into<String>) -> Self {
71 self.config = Some(path.into());
72 self
73 }
74
75 /// The env seam (the resolvers' only env reader).
76 #[must_use]
77 pub fn env(&self) -> &'a dyn EnvVars {
78 self.env
79 }
80
81 /// The `--database` override, if any.
82 #[must_use]
83 pub fn database_override(&self) -> Option<&str> {
84 self.database.as_deref()
85 }
86
87 /// Resolve the database path: `--database` > `DEGENBOT_DB_PATH` > the
88 /// built-in default. Never fails.
89 #[must_use]
90 pub fn database_path(&self) -> Resolved<PathBuf> {
91 resolve_database_path(self.env, self.database.as_deref())
92 }
93
94 /// Resolve the session chain id: `--chain-id` > `DEGENBOT_DEFAULT_CHAIN_ID`.
95 ///
96 /// # Errors
97 ///
98 /// [`degenbot_config::ConfigError`] when neither layer supplied a value, or
99 /// the winning layer is not an integer.
100 pub fn chain_id(&self) -> Result<Resolved<u64>, degenbot_config::ConfigError> {
101 resolve_chain_id(self.env, self.chain_id.as_deref())
102 }
103
104 /// Resolve the HTTP node URI for `chain_id`: `--node-http` >
105 /// `DEGENBOT_RPC_HTTP_CHAINID_<id>`.
106 ///
107 /// The updater arms resolve the chain they actually operate on (which may
108 /// differ from the session chain id, e.g. a validated Aave deployment).
109 ///
110 /// # Errors
111 ///
112 /// [`degenbot_config::ConfigError`] when no layer supplied the URI.
113 pub fn node_http_uri_for(
114 &self,
115 chain_id: u64,
116 ) -> Result<Resolved<String>, degenbot_config::ConfigError> {
117 resolve_node_http_uri(self.env, chain_id, self.node_http.as_deref())
118 }
119
120 /// Resolve the HTTP node URI for the session chain id.
121 ///
122 /// # Errors
123 ///
124 /// [`degenbot_config::ConfigError`] when the chain id or endpoint is
125 /// unresolved.
126 pub fn node_http_uri(&self) -> Result<Resolved<String>, degenbot_config::ConfigError> {
127 let chain_id = self.chain_id()?;
128 self.node_http_uri_for(chain_id.value)
129 }
130
131 /// Resolve both node URIs for the session chain id.
132 ///
133 /// # Errors
134 ///
135 /// [`degenbot_config::ConfigError`] when the chain id or either endpoint is
136 /// unresolved.
137 pub fn node_uris(&self) -> Result<ResolvedNodeUris, degenbot_config::ConfigError> {
138 let chain_id = self.chain_id()?;
139 resolve_node_uris(
140 self.env,
141 chain_id.value,
142 self.node_http.as_deref(),
143 self.node_ws.as_deref(),
144 )
145 }
146
147 /// Resolve the config file the strategy verbs write to: the `--config`
148 /// override, else the `DEGENBOT_CONFIG` env var (honored even when the
149 /// file is absent — the operator asked for it), else the XDG config home
150 /// (a write there creates the file). No home and no override is a typed
151 /// refusal, never a silent skip.
152 ///
153 /// # Errors
154 ///
155 /// [`CliError::Config`]-carrying [`crate::error::CliError`] when no
156 /// config location resolves at all.
157 pub fn resolve_config_file(&self) -> Result<std::path::PathBuf, crate::error::CliError> {
158 use crate::error::CliError;
159
160 if let Some(path) = &self.config {
161 return Ok(std::path::PathBuf::from(path));
162 }
163 if let Some(path) = degenbot_config::standard_file_path_with(self.env) {
164 return Ok(path);
165 }
166 if let Some(home) = degenbot_config::config_home(self.env) {
167 return Ok(home.join("degenbot").join("config.toml"));
168 }
169 Err(CliError::InvalidArgument(
170 "no config file location: pass --config or set DEGENBOT_CONFIG".to_string(),
171 ))
172 }
173
174 /// Load the typed config over this context's env + the resolved file.
175 ///
176 /// # Errors
177 ///
178 /// The loader's fail-closed [`degenbot_config::ConfigError`] wrapped in
179 /// [`crate::error::CliError::InvalidArgument`].
180 pub fn load_bot_config(&self) -> Result<degenbot_config::LoadedConfig, crate::error::CliError> {
181 let file = self.resolve_config_file()?;
182 self.load_bot_config_at(&file)
183 }
184
185 /// Load the typed config over this context's env + an explicit file.
186 /// An absent file is the empty default config (a first write creates it);
187 /// an existing-but-unreadable file surfaces the loader's refusal.
188 ///
189 /// # Errors
190 ///
191 /// The loader's fail-closed [`degenbot_config::ConfigError`] wrapped in
192 /// [`crate::error::CliError::InvalidArgument`].
193 pub fn load_bot_config_at(
194 &self,
195 file: &std::path::Path,
196 ) -> Result<degenbot_config::LoadedConfig, crate::error::CliError> {
197 if !file.exists() {
198 return degenbot_config::BotConfigLoader::new()
199 .with_env_ref(self.env)
200 .load()
201 .map_err(|error| crate::error::CliError::InvalidArgument(error.to_string()));
202 }
203 degenbot_config::BotConfigLoader::new()
204 .with_config_path(file)
205 .with_env_ref(self.env)
206 .load()
207 .map_err(|error| crate::error::CliError::InvalidArgument(error.to_string()))
208 }
209}