openzeppelin_relayer/domain/relayer/solana/rpc/
handler.rs1use 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 pub fn new(rpc_methods: T) -> Self {
41 Self { rpc_methods }
42 }
43
44 pub async fn handle_request(
66 &self,
67 request: JsonRpcRequest<NetworkRpcRequest>,
68 ) -> Result<JsonRpcResponse<NetworkRpcResult>, SolanaRpcError> {
69 info!("Received request params: {:?}", request.params);
70 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 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 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(), }))
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(), 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(), },
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}