openzeppelin_relayer/domain/relayer/solana/rpc/
handler.rs

1//! Handles incoming Solana RPC requests.
2//!
3//! This module defines the `SolanaRpcHandler` struct that dispatches RPC requests
4//! to the appropriate methods. It uses the trait defined in the `methods`
5//! module to process specific operations such as fee estimation, transaction
6//! preparation, signing, sending, and token retrieval.
7//!
8//! The handler converts JSON-RPC requests into concrete call parameters and then
9//! invokes the respective methods of the underlying implementation.
10use super::{SolanaRpcError, SolanaRpcMethods};
11use crate::{
12    domain::SolanaRpcMethodsImpl,
13    models::{
14        JsonRpcRequest, JsonRpcResponse, NetworkRpcRequest, NetworkRpcResult, SolanaRpcRequest,
15        SolanaRpcResult,
16    },
17};
18use eyre::Result;
19use std::sync::Arc;
20use tracing::debug;
21
22pub type SolanaRpcHandlerType<SP, S, JS, J, TR> =
23    Arc<SolanaRpcHandler<SolanaRpcMethodsImpl<SP, S, JS, J, TR>>>;
24
25pub struct SolanaRpcHandler<T> {
26    rpc_methods: T,
27}
28
29impl<T: SolanaRpcMethods> SolanaRpcHandler<T> {
30    /// Creates a new `SolanaRpcHandler` with the specified RPC methods.
31    ///
32    /// # Arguments
33    ///
34    /// * `rpc_methods` - An implementation of the `SolanaRpcMethods` trait that provides the
35    ///   necessary methods for handling RPC requests.
36    ///
37    /// # Returns
38    ///
39    /// Returns a new instance of `SolanaRpcHandler`
40    pub fn new(rpc_methods: T) -> Self {
41        Self { rpc_methods }
42    }
43
44    /// Handles an incoming JSON-RPC request and dispatches it to the appropriate method.
45    ///
46    /// This function processes the request by determining the method to call based on
47    /// the request's method name, deserializing the parameters, and invoking the corresponding
48    /// method on the `rpc_methods` implementation.
49    ///
50    /// # Arguments
51    ///
52    /// * `request` - A `JsonRpcRequest` containing the method name and parameters.
53    ///
54    /// # Returns
55    ///
56    /// Returns a `Result` containing either a `JsonRpcResponse` with the result of the method call
57    /// or a `SolanaRpcError` if an error occurred.
58    ///
59    /// # Errors
60    ///
61    /// This function will return an error if:
62    /// * The method is unsupported.
63    /// * The parameters cannot be deserialized.
64    /// * The underlying method call fails.
65    pub async fn handle_request(
66        &self,
67        request: JsonRpcRequest<NetworkRpcRequest>,
68    ) -> Result<JsonRpcResponse<NetworkRpcResult>, SolanaRpcError> {
69        debug!(params = ?request.params, "received request params");
70        // Extract Solana request or return error
71        let solana_request = match request.params {
72            NetworkRpcRequest::Solana(solana_params) => solana_params,
73            _ => {
74                return Err(SolanaRpcError::BadRequest(
75                    "Expected Solana network request".to_string(),
76                ));
77            }
78        };
79
80        let result = match solana_request {
81            SolanaRpcRequest::FeeEstimate(params) => {
82                let res = self.rpc_methods.fee_estimate(params).await?;
83                SolanaRpcResult::FeeEstimate(res)
84            }
85            SolanaRpcRequest::TransferTransaction(params) => {
86                let res = self.rpc_methods.transfer_transaction(params).await?;
87                SolanaRpcResult::TransferTransaction(res)
88            }
89            SolanaRpcRequest::PrepareTransaction(params) => {
90                let res = self.rpc_methods.prepare_transaction(params).await?;
91                SolanaRpcResult::PrepareTransaction(res)
92            }
93            SolanaRpcRequest::SignAndSendTransaction(params) => {
94                let res = self.rpc_methods.sign_and_send_transaction(params).await?;
95                SolanaRpcResult::SignAndSendTransaction(res)
96            }
97            SolanaRpcRequest::SignTransaction(params) => {
98                let res = self.rpc_methods.sign_transaction(params).await?;
99                SolanaRpcResult::SignTransaction(res)
100            }
101            SolanaRpcRequest::GetSupportedTokens(params) => {
102                let res = self.rpc_methods.get_supported_tokens(params).await?;
103                SolanaRpcResult::GetSupportedTokens(res)
104            }
105            SolanaRpcRequest::GetFeaturesEnabled(params) => {
106                let res = self.rpc_methods.get_features_enabled(params).await?;
107                SolanaRpcResult::GetFeaturesEnabled(res)
108            }
109            _ => {
110                return Err(SolanaRpcError::Internal(
111                    "Unsupported Solana RPC Paymaster method".to_string(),
112                ))
113            }
114        };
115
116        Ok(JsonRpcResponse::result(
117            request.id,
118            NetworkRpcResult::Solana(result),
119        ))
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use std::sync::Arc;
126
127    use crate::{
128        domain::MockSolanaRpcMethods,
129        models::{
130            EncodedSerializedTransaction, FeeEstimateRequestParams, FeeEstimateResult,
131            GetFeaturesEnabledRequestParams, GetFeaturesEnabledResult, JsonRpcId,
132            PrepareTransactionRequestParams, PrepareTransactionResult,
133            SignAndSendTransactionRequestParams, SignAndSendTransactionResult,
134            SignTransactionRequestParams, SignTransactionResult, TransferTransactionRequestParams,
135            TransferTransactionResult,
136        },
137    };
138
139    use super::*;
140    use mockall::predicate::{self};
141
142    #[tokio::test]
143    async fn test_handle_request_fee_estimate() {
144        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
145        mock_rpc_methods
146            .expect_fee_estimate()
147            .with(predicate::eq(FeeEstimateRequestParams {
148                transaction: EncodedSerializedTransaction::new("test_transaction".to_string()),
149                fee_token: "test_token".to_string(),
150            }))
151            .returning(|_| {
152                Ok(FeeEstimateResult {
153                    estimated_fee: "0".to_string(),
154                    conversion_rate: "0".to_string(),
155                })
156            })
157            .times(1);
158        let mock_handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
159        let request = JsonRpcRequest {
160            jsonrpc: "2.0".to_string(),
161            id: Some(JsonRpcId::Number(1)),
162            params: NetworkRpcRequest::Solana(SolanaRpcRequest::FeeEstimate(
163                FeeEstimateRequestParams {
164                    transaction: EncodedSerializedTransaction::new("test_transaction".to_string()),
165                    fee_token: "test_token".to_string(),
166                },
167            )),
168        };
169
170        let response = mock_handler.handle_request(request).await;
171
172        assert!(response.is_ok(), "Expected Ok response, got {:?}", response);
173        let json_response = response.unwrap();
174        assert_eq!(
175            json_response.result,
176            Some(NetworkRpcResult::Solana(SolanaRpcResult::FeeEstimate(
177                FeeEstimateResult {
178                    estimated_fee: "0".to_string(),
179                    conversion_rate: "0".to_string(),
180                }
181            )))
182        );
183    }
184
185    #[tokio::test]
186    async fn test_handle_request_features_enabled() {
187        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
188        mock_rpc_methods
189            .expect_get_features_enabled()
190            .with(predicate::eq(GetFeaturesEnabledRequestParams {}))
191            .returning(|_| {
192                Ok(GetFeaturesEnabledResult {
193                    features: vec!["gasless".to_string()],
194                })
195            })
196            .times(1);
197        let mock_handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
198        let request = JsonRpcRequest {
199            jsonrpc: "2.0".to_string(),
200            id: Some(JsonRpcId::Number(1)),
201            params: NetworkRpcRequest::Solana(SolanaRpcRequest::GetFeaturesEnabled(
202                GetFeaturesEnabledRequestParams {},
203            )),
204        };
205
206        let response = mock_handler.handle_request(request).await;
207
208        assert!(response.is_ok(), "Expected Ok response, got {:?}", response);
209        let json_response = response.unwrap();
210        assert_eq!(
211            json_response.result,
212            Some(NetworkRpcResult::Solana(
213                SolanaRpcResult::GetFeaturesEnabled(GetFeaturesEnabledResult {
214                    features: vec!["gasless".to_string()],
215                })
216            ))
217        );
218    }
219
220    #[tokio::test]
221    async fn test_handle_request_sign_transaction() {
222        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
223
224        // Create mock response
225        let mock_signature = "5wHu1qwD4kF3wxjejXkgDYNVnEgB1e8uVvrxNwJYRzHPPxWqRA4nxwE1TU4";
226        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
227
228        mock_rpc_methods
229            .expect_sign_transaction()
230            .with(predicate::eq(SignTransactionRequestParams {
231                transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
232            }))
233            .returning(move |_| {
234                Ok(SignTransactionResult {
235                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
236                    signature: mock_signature.to_string(),
237                })
238            })
239            .times(1);
240
241        let mock_handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
242
243        let request = JsonRpcRequest {
244            jsonrpc: "2.0".to_string(),
245            id: Some(JsonRpcId::Number(1)),
246            params: NetworkRpcRequest::Solana(SolanaRpcRequest::SignTransaction(
247                SignTransactionRequestParams {
248                    transaction: EncodedSerializedTransaction::new("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string()),
249                },
250            )),
251        };
252
253        let response = mock_handler.handle_request(request).await;
254
255        assert!(response.is_ok(), "Expected Ok response, got {:?}", response);
256        let json_response = response.unwrap();
257
258        match json_response.result {
259            Some(value) => {
260                if let NetworkRpcResult::Solana(SolanaRpcResult::SignTransaction(result)) = value {
261                    assert_eq!(result.signature, mock_signature);
262                } else {
263                    panic!("Expected SignTransaction result, got {:?}", value);
264                }
265            }
266            None => panic!("Expected Some result, got None"),
267        }
268    }
269
270    #[tokio::test]
271    async fn test_handle_request_sign_and_send_transaction_success() {
272        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
273
274        // Create mock data
275        let mock_signature = "5wHu1qwD4kF3wxjejXkgDYNVnEgB1e8uVvrxNwJYRzHPPxWqRA4nxwE1TU4";
276        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
277
278        mock_rpc_methods
279            .expect_sign_and_send_transaction()
280            .with(predicate::eq(SignAndSendTransactionRequestParams {
281                transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
282            }))
283            .returning(move |_| {
284                Ok(SignAndSendTransactionResult {
285                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
286                    signature: mock_signature.to_string(),
287                    id: "123".to_string(),
288                })
289            })
290            .times(1);
291
292        let handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
293
294        let request = JsonRpcRequest {
295            jsonrpc: "2.0".to_string(),
296            id: Some(JsonRpcId::Number(1)),
297            params: NetworkRpcRequest::Solana(SolanaRpcRequest::SignAndSendTransaction(
298                SignAndSendTransactionRequestParams {
299                    transaction: EncodedSerializedTransaction::new("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string()),
300                },
301            )),
302        };
303
304        let response = handler.handle_request(request).await;
305
306        assert!(response.is_ok());
307        let json_response = response.unwrap();
308        match json_response.result {
309            Some(value) => {
310                if let NetworkRpcResult::Solana(SolanaRpcResult::SignAndSendTransaction(result)) =
311                    value
312                {
313                    assert_eq!(result.signature, mock_signature);
314                } else {
315                    panic!("Expected SignAndSendTransaction result, got {:?}", value);
316                }
317            }
318            None => panic!("Expected Some result, got None"),
319        }
320    }
321
322    #[tokio::test]
323    async fn test_transfer_transaction_success() {
324        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
325        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
326
327        mock_rpc_methods
328            .expect_transfer_transaction()
329            .with(predicate::eq(TransferTransactionRequestParams {
330                source: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
331                destination: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
332                amount: 10,
333                token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(), // noboost
334            }))
335            .returning(move |_| {
336                Ok(TransferTransactionResult {
337                    fee_in_lamports: "1005000".to_string(),
338                    fee_in_spl: "1005000".to_string(),
339                    fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(), // noboost
340                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
341                    valid_until_blockheight: 351207983,
342                })
343            })
344            .times(1);
345
346        let handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
347
348        let request = JsonRpcRequest {
349            jsonrpc: "2.0".to_string(),
350            id: Some(JsonRpcId::Number(1)),
351            params: NetworkRpcRequest::Solana(SolanaRpcRequest::TransferTransaction(
352                TransferTransactionRequestParams {
353                    source: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
354                    destination: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
355                    amount: 10,
356                    token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(), // noboost
357                },
358            )),
359        };
360
361        let response = handler.handle_request(request).await;
362
363        assert!(response.is_ok());
364        let json_response = response.unwrap();
365        match json_response.result {
366            Some(value) => {
367                if let NetworkRpcResult::Solana(SolanaRpcResult::TransferTransaction(result)) =
368                    value
369                {
370                    assert!(!result.fee_in_lamports.is_empty());
371                    assert!(!result.fee_in_spl.is_empty());
372                    assert!(!result.fee_token.is_empty());
373                    assert!(!result.transaction.into_inner().is_empty());
374                    assert!(result.valid_until_blockheight > 0);
375                } else {
376                    panic!("Expected TransferTransaction result, got {:?}", value);
377                }
378            }
379            None => panic!("Expected Some result, got None"),
380        }
381    }
382
383    #[tokio::test]
384    async fn test_prepare_transaction_success() {
385        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
386        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
387
388        mock_rpc_methods
389            .expect_prepare_transaction()
390            .with(predicate::eq(PrepareTransactionRequestParams {
391                transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
392                fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(),
393            }))
394            .returning(move |_| {
395                Ok(PrepareTransactionResult {
396                    fee_in_lamports: "1005000".to_string(),
397                    fee_in_spl: "1005000".to_string(),
398                    fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(),
399                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
400                    valid_until_blockheight: 351207983,
401                })
402            })
403            .times(1);
404
405        let handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
406
407        let request = JsonRpcRequest {
408            jsonrpc: "2.0".to_string(),
409            id: Some(JsonRpcId::Number(1)),
410            params: NetworkRpcRequest::Solana(SolanaRpcRequest::PrepareTransaction(
411                PrepareTransactionRequestParams {
412                    transaction: EncodedSerializedTransaction::new("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string()),
413                    fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(),
414                },
415            )),
416        };
417
418        let response = handler.handle_request(request).await;
419
420        assert!(response.is_ok());
421        let json_response = response.unwrap();
422        match json_response.result {
423            Some(value) => {
424                if let NetworkRpcResult::Solana(SolanaRpcResult::PrepareTransaction(result)) = value
425                {
426                    assert!(!result.fee_in_lamports.is_empty());
427                    assert!(!result.fee_in_spl.is_empty());
428                    assert!(!result.fee_token.is_empty());
429                    assert!(!result.transaction.into_inner().is_empty());
430                    assert!(result.valid_until_blockheight > 0);
431                } else {
432                    panic!("Expected PrepareTransaction result, got {:?}", value);
433                }
434            }
435            None => panic!("Expected Some result, got None"),
436        }
437    }
438}