openzeppelin_relayer/domain/relayer/solana/rpc/
handler.rs1use 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 pub fn new(rpc_methods: T) -> Self {
34 Self { rpc_methods }
35 }
36
37 pub async fn handle_request(
59 &self,
60 request: JsonRpcRequest<NetworkRpcRequest>,
61 ) -> Result<JsonRpcResponse<NetworkRpcResult>, SolanaRpcError> {
62 info!("Received request params: {:?}", request.params);
63 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 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 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(), }))
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(), 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(), },
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}