riglr_core/provider.rs
1//! Chain-agnostic application context for dependency injection
2//!
3//! This module provides the `ApplicationContext` pattern for managing shared resources
4//! and dependencies across the application without circular dependencies.
5//!
6//! # Architecture
7//!
8//! The ApplicationContext enables riglr-core to remain chain-agnostic by using
9//! type erasure and dependency injection. Concrete blockchain implementations
10//! are injected at runtime by the application layer.
11//!
12//! # Dependency Flow
13//!
14//! ```text
15//! Application Layer (creates clients)
16//! ↓
17//! ApplicationContext (stores as Arc<dyn Any>)
18//! ↓
19//! Tools/Workers (retrieve by type)
20//! ```
21
22use dashmap::DashMap;
23use riglr_config::Config;
24use std::any::{Any, TypeId};
25use std::sync::Arc;
26use std::time::Duration;
27
28use crate::util::RateLimiter;
29
30/// Chain-agnostic context for dependency injection and resource management.
31///
32/// The ApplicationContext serves as a dependency injection container for
33/// the riglr ecosystem, enabling tools and workers to access shared resources
34/// (RPC clients, signers, database connections) without creating circular dependencies.
35///
36/// # Chain-Agnostic Design
37///
38/// The context uses type erasure (`Arc<dyn Any>`) to store blockchain clients
39/// without depending on their concrete types. This allows riglr-core to remain
40/// independent of blockchain SDKs while still providing access to them at runtime.
41///
42/// # Extension System
43///
44/// Resources are stored as "extensions" - type-erased objects that can be
45/// retrieved by their original type. This enables clean separation between
46/// riglr-core (which defines the interfaces) and chain-specific crates
47/// (which provide the implementations).
48///
49/// ## Accessing Chain-Specific Clients
50///
51/// To maintain its chain-agnostic nature, `riglr-core` does not have direct methods
52/// like `solana_client()` on the `ApplicationContext`. Instead, these ergonomic
53/// accessors are provided via extension traits in chain-specific crates.
54///
55/// For example, `riglr-solana-tools` provides the `SolanaAppContextProvider` trait,
56/// which adds the `.solana_client()` method to `ApplicationContext`.
57///
58/// See the [`riglr_core::provider_extensions`] module for more details on this pattern.
59///
60/// # Examples
61///
62/// ```rust,no_run
63/// use riglr_core::provider::ApplicationContext;
64/// use riglr_config::ConfigBuilder;
65/// use std::sync::Arc;
66///
67/// // Application layer creates context and injects dependencies
68/// let config = ConfigBuilder::default().build().unwrap();
69/// let context = ApplicationContext::from_config(&config);
70///
71/// // Inject Solana RPC client (in real code, from riglr-solana-tools)
72/// // let solana_client = Arc::new(solana_client::rpc_client::RpcClient::new(...));
73/// // context.set_extension(solana_client.clone());
74///
75/// // Inject EVM provider (in real code, from riglr-evm-tools)
76/// // let evm_provider = Arc::new(alloy::providers::Provider::new(...));
77/// // context.set_extension(evm_provider.clone());
78///
79/// // Tools retrieve clients by type
80/// // let client: Option<Arc<RpcClient>> = context.get_extension();
81/// ```
82#[derive(Clone, Debug)]
83pub struct ApplicationContext {
84 /// Configuration for the application
85 pub config: Config,
86 /// Rate limiter for controlling request rates per client/user
87 pub rate_limiter: Arc<RateLimiter>,
88 /// Type-safe extensions for storing arbitrary shared resources
89 extensions: Arc<DashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
90}
91
92impl ApplicationContext {
93 /// Create a new ApplicationContext from configuration
94 pub fn from_config(config: &Config) -> Self {
95 // Initialize rate limiter with default values
96 // Default: 100 requests per minute per client
97 let rate_limiter = Arc::new(RateLimiter::new(100, Duration::from_secs(60)));
98
99 Self {
100 config: config.clone(),
101 rate_limiter,
102 extensions: Arc::new(DashMap::new()),
103 }
104 }
105
106 /// Create a new ApplicationContext from environment variables
107 ///
108 /// **DEPRECATED**: Configuration should be loaded in the application binary using
109 /// `riglr_config::Config::from_env()` and passed to `ApplicationContext::from_config()`.
110 /// This ensures proper separation of concerns where `riglr-core` consumes configuration
111 /// but does not load it, reinforcing the unidirectional dependency flow.
112 ///
113 /// # Migration Guide
114 ///
115 /// Instead of:
116 /// ```rust,no_run
117 /// use riglr_core::provider::ApplicationContext;
118 /// let context = ApplicationContext::from_env();
119 /// ```
120 ///
121 /// Use:
122 /// ```rust,no_run
123 /// use riglr_core::provider::ApplicationContext;
124 /// use riglr_config::Config;
125 ///
126 /// let config = Config::from_env();
127 /// let context = ApplicationContext::from_config(&config);
128 /// ```
129 #[deprecated(
130 since = "0.3.0",
131 note = "Use Config::from_env() followed by ApplicationContext::from_config() instead. This ensures proper separation of concerns."
132 )]
133 pub fn from_env() -> Self {
134 let config = Config::from_env();
135 Self::from_config(&config)
136 }
137
138 /// Add an extension to the context
139 ///
140 /// Extensions are stored by their type, allowing type-safe retrieval later.
141 /// This is the recommended pattern for injecting RPC clients and other resources.
142 ///
143 /// # Examples
144 ///
145 /// ```rust,no_run
146 /// use riglr_core::provider::ApplicationContext;
147 /// use riglr_config::ConfigBuilder;
148 /// use std::sync::Arc;
149 ///
150 /// let config = ConfigBuilder::default().build().unwrap();
151 /// let context = ApplicationContext::from_config(&config);
152 ///
153 /// // Add blockchain RPC clients as extensions
154 /// // Example: Add Solana RPC client
155 /// // let solana_client = Arc::new(solana_client::rpc_client::RpcClient::new(...));
156 /// // context.set_extension(solana_client);
157 ///
158 /// // Example: Add EVM provider
159 /// // let evm_provider = Arc::new(alloy::Provider::new(...));
160 /// // context.set_extension(evm_provider);
161 /// ```
162 pub fn set_extension<T: Send + Sync + 'static>(&self, extension: Arc<T>) {
163 self.extensions.insert(TypeId::of::<T>(), extension);
164 }
165
166 /// Get an extension by type
167 ///
168 /// Returns None if no extension of the given type has been set.
169 ///
170 /// # Examples
171 ///
172 /// ```rust,no_run
173 /// use riglr_core::provider::ApplicationContext;
174 /// use riglr_config::ConfigBuilder;
175 /// use std::sync::Arc;
176 ///
177 /// let config = ConfigBuilder::default().build().unwrap();
178 /// let context = ApplicationContext::from_config(&config);
179 /// // Add a typed extension (e.g., an RPC client)
180 /// // let client = Arc::new(MyRpcClient::new(...));
181 /// // context.set_extension(client.clone());
182 ///
183 /// // Retrieve the client later by type
184 /// // let retrieved: Arc<MyRpcClient> = context.get_extension()
185 /// // .expect("RPC client not found");
186 /// ```
187 pub fn get_extension<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
188 self.extensions
189 .get(&TypeId::of::<T>())
190 .and_then(|ext| ext.clone().downcast::<T>().ok())
191 }
192
193 /// Check if an extension of the given type exists
194 pub fn has_extension<T: Send + Sync + 'static>(&self) -> bool {
195 self.extensions.contains_key(&TypeId::of::<T>())
196 }
197
198 /// Remove an extension by type
199 ///
200 /// Returns the removed extension if it existed.
201 pub fn remove_extension<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
202 self.extensions
203 .remove(&TypeId::of::<T>())
204 .and_then(|(_, ext)| ext.downcast::<T>().ok())
205 }
206
207 /// Clear all extensions
208 pub fn clear_extensions(&self) {
209 self.extensions.clear();
210 }
211
212 /// Get the number of extensions
213 pub fn extension_count(&self) -> usize {
214 self.extensions.len()
215 }
216}
217
218impl Default for ApplicationContext {
219 fn default() -> Self {
220 // Create with an empty/default configuration using builder
221 // Users should use from_config() for production use
222 let config = riglr_config::ConfigBuilder::new()
223 .build()
224 .expect("Default config should be valid");
225 let rate_limiter = Arc::new(RateLimiter::new(100, Duration::from_secs(60)));
226
227 Self {
228 config,
229 rate_limiter,
230 extensions: Arc::new(DashMap::new()),
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[derive(Debug, Clone)]
240 struct TestResource {
241 value: String,
242 }
243
244 #[test]
245 fn test_application_context_extensions() {
246 let context = ApplicationContext::default();
247
248 // Test adding and retrieving an extension
249 let resource = Arc::new(TestResource {
250 value: "test".to_string(),
251 });
252 context.set_extension(resource.clone());
253
254 let retrieved: Arc<TestResource> = context.get_extension().expect("Resource not found");
255 assert_eq!(retrieved.value, "test");
256 }
257
258 #[test]
259 fn test_application_context_multiple_extensions() {
260 let context = ApplicationContext::default();
261
262 // Add multiple different types
263 let resource1 = Arc::new(TestResource {
264 value: "test1".to_string(),
265 });
266 let resource2 = Arc::new(42u32);
267
268 context.set_extension(resource1.clone());
269 context.set_extension(resource2.clone());
270
271 // Retrieve both
272 let retrieved1: Arc<TestResource> = context.get_extension().expect("Resource not found");
273 let retrieved2: Arc<u32> = context.get_extension().expect("u32 not found");
274
275 assert_eq!(retrieved1.value, "test1");
276 assert_eq!(*retrieved2, 42);
277 }
278
279 #[test]
280 fn test_application_context_has_extension() {
281 let context = ApplicationContext::default();
282
283 assert!(!context.has_extension::<TestResource>());
284
285 let resource = Arc::new(TestResource {
286 value: "test".to_string(),
287 });
288 context.set_extension(resource);
289
290 assert!(context.has_extension::<TestResource>());
291 }
292
293 #[test]
294 fn test_application_context_remove_extension() {
295 let context = ApplicationContext::default();
296
297 let resource = Arc::new(TestResource {
298 value: "test".to_string(),
299 });
300 context.set_extension(resource);
301
302 assert!(context.has_extension::<TestResource>());
303
304 let removed: Option<Arc<TestResource>> = context.remove_extension();
305 assert!(removed.is_some());
306 assert!(!context.has_extension::<TestResource>());
307 }
308}