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 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 pub fn rpc_methods(&self) -> &T {
32 &self.rpc_methods
33 }
34}
35
36impl<T: SolanaRpcMethods> SolanaRpcHandler<T> {
37 pub fn new(rpc_methods: T) -> Self {
48 Self { rpc_methods }
49 }
50
51 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 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 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 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(), }))
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(), 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(), },
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}