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