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 log::info;
20use std::sync::Arc;
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        info!("Received request params: {:?}", 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
111        Ok(JsonRpcResponse::result(
112            request.id,
113            NetworkRpcResult::Solana(result),
114        ))
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use std::sync::Arc;
121
122    use crate::{
123        domain::MockSolanaRpcMethods,
124        models::{
125            EncodedSerializedTransaction, FeeEstimateRequestParams, FeeEstimateResult,
126            GetFeaturesEnabledRequestParams, GetFeaturesEnabledResult, JsonRpcId,
127            PrepareTransactionRequestParams, PrepareTransactionResult,
128            SignAndSendTransactionRequestParams, SignAndSendTransactionResult,
129            SignTransactionRequestParams, SignTransactionResult, TransferTransactionRequestParams,
130            TransferTransactionResult,
131        },
132    };
133
134    use super::*;
135    use mockall::predicate::{self};
136
137    #[tokio::test]
138    async fn test_handle_request_fee_estimate() {
139        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
140        mock_rpc_methods
141            .expect_fee_estimate()
142            .with(predicate::eq(FeeEstimateRequestParams {
143                transaction: EncodedSerializedTransaction::new("test_transaction".to_string()),
144                fee_token: "test_token".to_string(),
145            }))
146            .returning(|_| {
147                Ok(FeeEstimateResult {
148                    estimated_fee: "0".to_string(),
149                    conversion_rate: "0".to_string(),
150                })
151            })
152            .times(1);
153        let mock_handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
154        let request = JsonRpcRequest {
155            jsonrpc: "2.0".to_string(),
156            id: Some(JsonRpcId::Number(1)),
157            params: NetworkRpcRequest::Solana(SolanaRpcRequest::FeeEstimate(
158                FeeEstimateRequestParams {
159                    transaction: EncodedSerializedTransaction::new("test_transaction".to_string()),
160                    fee_token: "test_token".to_string(),
161                },
162            )),
163        };
164
165        let response = mock_handler.handle_request(request).await;
166
167        assert!(response.is_ok(), "Expected Ok response, got {:?}", response);
168        let json_response = response.unwrap();
169        assert_eq!(
170            json_response.result,
171            Some(NetworkRpcResult::Solana(SolanaRpcResult::FeeEstimate(
172                FeeEstimateResult {
173                    estimated_fee: "0".to_string(),
174                    conversion_rate: "0".to_string(),
175                }
176            )))
177        );
178    }
179
180    #[tokio::test]
181    async fn test_handle_request_features_enabled() {
182        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
183        mock_rpc_methods
184            .expect_get_features_enabled()
185            .with(predicate::eq(GetFeaturesEnabledRequestParams {}))
186            .returning(|_| {
187                Ok(GetFeaturesEnabledResult {
188                    features: vec!["gasless".to_string()],
189                })
190            })
191            .times(1);
192        let mock_handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
193        let request = JsonRpcRequest {
194            jsonrpc: "2.0".to_string(),
195            id: Some(JsonRpcId::Number(1)),
196            params: NetworkRpcRequest::Solana(SolanaRpcRequest::GetFeaturesEnabled(
197                GetFeaturesEnabledRequestParams {},
198            )),
199        };
200
201        let response = mock_handler.handle_request(request).await;
202
203        assert!(response.is_ok(), "Expected Ok response, got {:?}", response);
204        let json_response = response.unwrap();
205        assert_eq!(
206            json_response.result,
207            Some(NetworkRpcResult::Solana(
208                SolanaRpcResult::GetFeaturesEnabled(GetFeaturesEnabledResult {
209                    features: vec!["gasless".to_string()],
210                })
211            ))
212        );
213    }
214
215    #[tokio::test]
216    async fn test_handle_request_sign_transaction() {
217        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
218
219        // Create mock response
220        let mock_signature = "5wHu1qwD4kF3wxjejXkgDYNVnEgB1e8uVvrxNwJYRzHPPxWqRA4nxwE1TU4";
221        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
222
223        mock_rpc_methods
224            .expect_sign_transaction()
225            .with(predicate::eq(SignTransactionRequestParams {
226                transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
227            }))
228            .returning(move |_| {
229                Ok(SignTransactionResult {
230                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
231                    signature: mock_signature.to_string(),
232                })
233            })
234            .times(1);
235
236        let mock_handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
237
238        let request = JsonRpcRequest {
239            jsonrpc: "2.0".to_string(),
240            id: Some(JsonRpcId::Number(1)),
241            params: NetworkRpcRequest::Solana(SolanaRpcRequest::SignTransaction(
242                SignTransactionRequestParams {
243                    transaction: EncodedSerializedTransaction::new("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string()),
244                },
245            )),
246        };
247
248        let response = mock_handler.handle_request(request).await;
249
250        assert!(response.is_ok(), "Expected Ok response, got {:?}", response);
251        let json_response = response.unwrap();
252
253        match json_response.result {
254            Some(value) => {
255                if let NetworkRpcResult::Solana(SolanaRpcResult::SignTransaction(result)) = value {
256                    assert_eq!(result.signature, mock_signature);
257                } else {
258                    panic!("Expected SignTransaction result, got {:?}", value);
259                }
260            }
261            None => panic!("Expected Some result, got None"),
262        }
263    }
264
265    #[tokio::test]
266    async fn test_handle_request_sign_and_send_transaction_success() {
267        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
268
269        // Create mock data
270        let mock_signature = "5wHu1qwD4kF3wxjejXkgDYNVnEgB1e8uVvrxNwJYRzHPPxWqRA4nxwE1TU4";
271        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
272
273        mock_rpc_methods
274            .expect_sign_and_send_transaction()
275            .with(predicate::eq(SignAndSendTransactionRequestParams {
276                transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
277            }))
278            .returning(move |_| {
279                Ok(SignAndSendTransactionResult {
280                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
281                    signature: mock_signature.to_string(),
282                    id: "123".to_string(),
283                })
284            })
285            .times(1);
286
287        let handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
288
289        let request = JsonRpcRequest {
290            jsonrpc: "2.0".to_string(),
291            id: Some(JsonRpcId::Number(1)),
292            params: NetworkRpcRequest::Solana(SolanaRpcRequest::SignAndSendTransaction(
293                SignAndSendTransactionRequestParams {
294                    transaction: EncodedSerializedTransaction::new("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string()),
295                },
296            )),
297        };
298
299        let response = handler.handle_request(request).await;
300
301        assert!(response.is_ok());
302        let json_response = response.unwrap();
303        match json_response.result {
304            Some(value) => {
305                if let NetworkRpcResult::Solana(SolanaRpcResult::SignAndSendTransaction(result)) =
306                    value
307                {
308                    assert_eq!(result.signature, mock_signature);
309                } else {
310                    panic!("Expected SignAndSendTransaction result, got {:?}", value);
311                }
312            }
313            None => panic!("Expected Some result, got None"),
314        }
315    }
316
317    #[tokio::test]
318    async fn test_transfer_transaction_success() {
319        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
320        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
321
322        mock_rpc_methods
323            .expect_transfer_transaction()
324            .with(predicate::eq(TransferTransactionRequestParams {
325                source: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
326                destination: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
327                amount: 10,
328                token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(), // noboost
329            }))
330            .returning(move |_| {
331                Ok(TransferTransactionResult {
332                    fee_in_lamports: "1005000".to_string(),
333                    fee_in_spl: "1005000".to_string(),
334                    fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(), // noboost
335                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
336                    valid_until_blockheight: 351207983,
337                })
338            })
339            .times(1);
340
341        let handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
342
343        let request = JsonRpcRequest {
344            jsonrpc: "2.0".to_string(),
345            id: Some(JsonRpcId::Number(1)),
346            params: NetworkRpcRequest::Solana(SolanaRpcRequest::TransferTransaction(
347                TransferTransactionRequestParams {
348                    source: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
349                    destination: "C6VBV1EK2Jx7kFgCkCD5wuDeQtEH8ct2hHGUPzEhUSc8".to_string(),
350                    amount: 10,
351                    token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(), // noboost
352                },
353            )),
354        };
355
356        let response = handler.handle_request(request).await;
357
358        assert!(response.is_ok());
359        let json_response = response.unwrap();
360        match json_response.result {
361            Some(value) => {
362                if let NetworkRpcResult::Solana(SolanaRpcResult::TransferTransaction(result)) =
363                    value
364                {
365                    assert!(!result.fee_in_lamports.is_empty());
366                    assert!(!result.fee_in_spl.is_empty());
367                    assert!(!result.fee_token.is_empty());
368                    assert!(!result.transaction.into_inner().is_empty());
369                    assert!(result.valid_until_blockheight > 0);
370                } else {
371                    panic!("Expected TransferTransaction result, got {:?}", value);
372                }
373            }
374            None => panic!("Expected Some result, got None"),
375        }
376    }
377
378    #[tokio::test]
379    async fn test_prepare_transaction_success() {
380        let mut mock_rpc_methods = MockSolanaRpcMethods::new();
381        let mock_transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string();
382
383        mock_rpc_methods
384            .expect_prepare_transaction()
385            .with(predicate::eq(PrepareTransactionRequestParams {
386                transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
387                fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(),
388            }))
389            .returning(move |_| {
390                Ok(PrepareTransactionResult {
391                    fee_in_lamports: "1005000".to_string(),
392                    fee_in_spl: "1005000".to_string(),
393                    fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(),
394                    transaction: EncodedSerializedTransaction::new(mock_transaction.clone()),
395                    valid_until_blockheight: 351207983,
396                })
397            })
398            .times(1);
399
400        let handler = Arc::new(SolanaRpcHandler::new(mock_rpc_methods));
401
402        let request = JsonRpcRequest {
403            jsonrpc: "2.0".to_string(),
404            id: Some(JsonRpcId::Number(1)),
405            params: NetworkRpcRequest::Solana(SolanaRpcRequest::PrepareTransaction(
406                PrepareTransactionRequestParams {
407                    transaction: EncodedSerializedTransaction::new("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string()),
408                    fee_token: "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr".to_string(),
409                },
410            )),
411        };
412
413        let response = handler.handle_request(request).await;
414
415        assert!(response.is_ok());
416        let json_response = response.unwrap();
417        match json_response.result {
418            Some(value) => {
419                if let NetworkRpcResult::Solana(SolanaRpcResult::PrepareTransaction(result)) = value
420                {
421                    assert!(!result.fee_in_lamports.is_empty());
422                    assert!(!result.fee_in_spl.is_empty());
423                    assert!(!result.fee_token.is_empty());
424                    assert!(!result.transaction.into_inner().is_empty());
425                    assert!(result.valid_until_blockheight > 0);
426                } else {
427                    panic!("Expected PrepareTransaction result, got {:?}", value);
428                }
429            }
430            None => panic!("Expected Some result, got None"),
431        }
432    }
433}