1use std::sync::Arc;
4
5use crate::{
6 jobs::JobProducerTrait,
7 models::{
8 AppState, NetworkRepoModel, NotificationRepoModel, PluginCallRequest, PluginModel,
9 RelayerRepoModel, SignerRepoModel, ThinDataAppState, TransactionRepoModel,
10 },
11 repositories::{
12 NetworkRepository, PluginRepositoryTrait, RelayerRepository, Repository,
13 TransactionCounterTrait, TransactionRepository,
14 },
15};
16use actix_web::web;
17use async_trait::async_trait;
18use serde::{Deserialize, Serialize};
19use thiserror::Error;
20use uuid::Uuid;
21
22pub mod runner;
23pub use runner::*;
24
25pub mod relayer_api;
26pub use relayer_api::*;
27
28pub mod script_executor;
29pub use script_executor::*;
30
31pub mod socket;
32pub use socket::*;
33
34#[cfg(test)]
35use mockall::automock;
36
37#[derive(Error, Debug, Serialize)]
38pub enum PluginError {
39 #[error("Socket error: {0}")]
40 SocketError(String),
41 #[error("Plugin error: {0}")]
42 PluginError(String),
43 #[error("Relayer error: {0}")]
44 RelayerError(String),
45 #[error("Plugin execution error: {0}")]
46 PluginExecutionError(String),
47 #[error("Script execution timed out after {0} seconds")]
48 ScriptTimeout(u64),
49 #[error("Invalid method: {0}")]
50 InvalidMethod(String),
51 #[error("Invalid payload: {0}")]
52 InvalidPayload(String),
53}
54
55impl From<PluginError> for String {
56 fn from(error: PluginError) -> Self {
57 error.to_string()
58 }
59}
60
61#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
62pub struct PluginCallResponse {
63 pub success: bool,
64 pub return_value: String,
65 pub message: String,
66 pub logs: Vec<LogEntry>,
67 pub error: String,
68 pub traces: Vec<serde_json::Value>,
69}
70
71#[derive(Default)]
72pub struct PluginService<R: PluginRunnerTrait> {
73 runner: R,
74}
75
76impl<R: PluginRunnerTrait> PluginService<R> {
77 pub fn new(runner: R) -> Self {
78 Self { runner }
79 }
80
81 fn resolve_plugin_path(plugin_path: &str) -> String {
82 if plugin_path.starts_with("plugins/") {
83 plugin_path.to_string()
84 } else {
85 format!("plugins/{}", plugin_path)
86 }
87 }
88
89 #[allow(clippy::type_complexity)]
90 async fn call_plugin<J, RR, TR, NR, NFR, SR, TCR, PR>(
91 &self,
92 plugin: PluginModel,
93 plugin_call_request: PluginCallRequest,
94 state: Arc<ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR>>,
95 ) -> Result<PluginCallResponse, PluginError>
96 where
97 J: JobProducerTrait + Send + Sync + 'static,
98 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
99 TR: TransactionRepository
100 + Repository<TransactionRepoModel, String>
101 + Send
102 + Sync
103 + 'static,
104 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
105 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
106 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
107 TCR: TransactionCounterTrait + Send + Sync + 'static,
108 PR: PluginRepositoryTrait + Send + Sync + 'static,
109 {
110 let socket_path = format!("/tmp/{}.sock", Uuid::new_v4());
111 let script_path = Self::resolve_plugin_path(&plugin.path);
112 let script_params = plugin_call_request.params.to_string();
113
114 let result = self
115 .runner
116 .run(
117 &socket_path,
118 script_path,
119 plugin.timeout,
120 script_params,
121 state,
122 )
123 .await;
124
125 match result {
126 Ok(script_result) => Ok(PluginCallResponse {
127 success: true,
128 message: "Plugin called successfully".to_string(),
129 return_value: script_result.return_value,
130 logs: script_result.logs,
131 error: script_result.error,
132 traces: script_result.trace,
133 }),
134 Err(e) => Err(PluginError::PluginExecutionError(e.to_string())),
135 }
136 }
137}
138
139#[async_trait]
140#[cfg_attr(test, automock)]
141pub trait PluginServiceTrait<J, TR, RR, NR, NFR, SR, TCR, PR>: Send + Sync
142where
143 J: JobProducerTrait + 'static,
144 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
145 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
146 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
147 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
148 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
149 TCR: TransactionCounterTrait + Send + Sync + 'static,
150 PR: PluginRepositoryTrait + Send + Sync + 'static,
151{
152 fn new(runner: PluginRunner) -> Self;
153 async fn call_plugin(
154 &self,
155 plugin: PluginModel,
156 plugin_call_request: PluginCallRequest,
157 state: Arc<web::ThinData<AppState<J, RR, TR, NR, NFR, SR, TCR, PR>>>,
158 ) -> Result<PluginCallResponse, PluginError>;
159}
160
161#[async_trait]
162impl<J, TR, RR, NR, NFR, SR, TCR, PR> PluginServiceTrait<J, TR, RR, NR, NFR, SR, TCR, PR>
163 for PluginService<PluginRunner>
164where
165 J: JobProducerTrait + 'static,
166 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
167 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
168 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
169 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
170 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
171 TCR: TransactionCounterTrait + Send + Sync + 'static,
172 PR: PluginRepositoryTrait + Send + Sync + 'static,
173{
174 fn new(runner: PluginRunner) -> Self {
175 Self::new(runner)
176 }
177
178 async fn call_plugin(
179 &self,
180 plugin: PluginModel,
181 plugin_call_request: PluginCallRequest,
182 state: Arc<web::ThinData<AppState<J, RR, TR, NR, NFR, SR, TCR, PR>>>,
183 ) -> Result<PluginCallResponse, PluginError> {
184 self.call_plugin(plugin, plugin_call_request, state).await
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use std::time::Duration;
191
192 use crate::{
193 constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS,
194 jobs::MockJobProducerTrait,
195 models::PluginModel,
196 repositories::{
197 NetworkRepositoryStorage, NotificationRepositoryStorage, PluginRepositoryStorage,
198 RelayerRepositoryStorage, SignerRepositoryStorage, TransactionCounterRepositoryStorage,
199 TransactionRepositoryStorage,
200 },
201 utils::mocks::mockutils::create_mock_app_state,
202 };
203
204 use super::*;
205
206 #[test]
207 fn test_resolve_plugin_path() {
208 assert_eq!(
209 PluginService::<MockPluginRunnerTrait>::resolve_plugin_path("plugins/examples/test.ts"),
210 "plugins/examples/test.ts"
211 );
212
213 assert_eq!(
214 PluginService::<MockPluginRunnerTrait>::resolve_plugin_path("examples/test.ts"),
215 "plugins/examples/test.ts"
216 );
217
218 assert_eq!(
219 PluginService::<MockPluginRunnerTrait>::resolve_plugin_path("test.ts"),
220 "plugins/test.ts"
221 );
222 }
223
224 #[tokio::test]
225 async fn test_call_plugin() {
226 let plugin = PluginModel {
227 id: "test-plugin".to_string(),
228 path: "test-path".to_string(),
229 timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
230 };
231 let app_state: AppState<
232 MockJobProducerTrait,
233 RelayerRepositoryStorage,
234 TransactionRepositoryStorage,
235 NetworkRepositoryStorage,
236 NotificationRepositoryStorage,
237 SignerRepositoryStorage,
238 TransactionCounterRepositoryStorage,
239 PluginRepositoryStorage,
240 > = create_mock_app_state(None, None, None, Some(vec![plugin.clone()]), None).await;
241
242 let mut plugin_runner = MockPluginRunnerTrait::default();
243
244 plugin_runner
245 .expect_run::<MockJobProducerTrait, RelayerRepositoryStorage, TransactionRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage, SignerRepositoryStorage, TransactionCounterRepositoryStorage, PluginRepositoryStorage>()
246 .returning(|_, _, _, _, _| {
247 Ok(ScriptResult {
248 logs: vec![LogEntry {
249 level: LogLevel::Log,
250 message: "test-log".to_string(),
251 }],
252 error: "test-error".to_string(),
253 return_value: "test-result".to_string(),
254 trace: Vec::new(),
255 })
256 });
257
258 let plugin_service = PluginService::<MockPluginRunnerTrait>::new(plugin_runner);
259 let result = plugin_service
260 .call_plugin(
261 plugin,
262 PluginCallRequest {
263 params: serde_json::Value::Null,
264 },
265 Arc::new(web::ThinData(app_state)),
266 )
267 .await;
268 assert!(result.is_ok());
269 let result = result.unwrap();
270 assert!(result.success);
271 assert_eq!(result.return_value, "test-result");
272 }
273
274 #[tokio::test]
275 async fn test_from_plugin_error_to_string() {
276 let error = PluginError::PluginExecutionError("test-error".to_string());
277 let result: String = error.into();
278 assert_eq!(result, "Plugin execution error: test-error");
279 }
280}