openzeppelin_relayer/services/plugins/
mod.rs

1//! Plugins service module for handling plugins execution and interaction with relayer
2
3use 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}