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