openzeppelin_relayer/models/transaction/request/
solana.rs1use crate::{
2 constants::{
3 REQUEST_MAX_ACCOUNTS_PER_INSTRUCTION, REQUEST_MAX_INSTRUCTIONS,
4 REQUEST_MAX_INSTRUCTION_DATA_SIZE, REQUEST_MAX_TOTAL_ACCOUNTS,
5 },
6 models::{ApiError, EncodedSerializedTransaction, RelayerRepoModel, SolanaInstructionSpec},
7 utils::base64_decode,
8};
9use serde::{Deserialize, Serialize};
10use solana_sdk::{pubkey::Pubkey, transaction::Transaction};
11use std::{collections::HashSet, str::FromStr};
12use utoipa::ToSchema;
13
14#[derive(Deserialize, Serialize, ToSchema)]
15pub struct SolanaTransactionRequest {
16 #[schema(nullable = true)]
18 pub transaction: Option<EncodedSerializedTransaction>,
19
20 #[schema(nullable = true)]
22 pub instructions: Option<Vec<SolanaInstructionSpec>>,
23
24 #[schema(nullable = true)]
26 pub valid_until: Option<String>,
27}
28
29impl SolanaTransactionRequest {
30 pub fn validate(&self, relayer: &RelayerRepoModel) -> Result<(), ApiError> {
31 let has_transaction = self.transaction.is_some();
32 let has_instructions = self
33 .instructions
34 .as_ref()
35 .map(|i| !i.is_empty())
36 .unwrap_or(false);
37
38 match (has_transaction, has_instructions) {
39 (true, true) => {
40 return Err(ApiError::BadRequest(
41 "Cannot provide both transaction and instructions".to_string(),
42 ));
43 }
44 (false, false) => {
45 return Err(ApiError::BadRequest(
46 "Must provide either transaction or instructions".to_string(),
47 ));
48 }
49 _ => {}
50 }
51
52 if let Some(ref transaction) = self.transaction {
54 Self::validate_transaction(transaction, relayer)?;
55 }
56
57 if let Some(ref instructions) = self.instructions {
59 Self::validate_instructions(instructions, relayer)?;
60 }
61
62 if let Some(valid_until) = &self.valid_until {
64 match chrono::DateTime::parse_from_rfc3339(valid_until) {
65 Ok(valid_until_dt) => {
66 if valid_until_dt <= chrono::Utc::now() {
67 return Err(ApiError::BadRequest(
68 "valid_until cannot be in the past".to_string(),
69 ));
70 }
71 }
72 Err(_) => {
73 return Err(ApiError::BadRequest(
74 "valid_until must be a valid RFC3339 timestamp".to_string(),
75 ));
76 }
77 }
78 }
79
80 Ok(())
81 }
82
83 fn validate_transaction(
89 transaction: &EncodedSerializedTransaction,
90 relayer: &RelayerRepoModel,
91 ) -> Result<(), ApiError> {
92 let tx = Transaction::try_from(transaction.clone())
94 .map_err(|e| ApiError::BadRequest(format!("Failed to decode transaction: {e}")))?;
95
96 let fee_payer = tx.message.account_keys.first().ok_or_else(|| {
98 ApiError::BadRequest("Transaction has no fee payer account".to_string())
99 })?;
100
101 let relayer_pubkey = Pubkey::from_str(&relayer.address)
103 .map_err(|e| ApiError::BadRequest(format!("Invalid relayer address: {e}")))?;
104
105 if fee_payer != &relayer_pubkey {
107 return Err(ApiError::BadRequest(format!(
108 "Transaction fee payer {fee_payer} does not match relayer address {relayer_pubkey}"
109 )));
110 }
111
112 Ok(())
113 }
114
115 fn validate_instructions(
126 instructions: &[SolanaInstructionSpec],
127 relayer: &RelayerRepoModel,
128 ) -> Result<(), ApiError> {
129 let relayer_pubkey = Pubkey::from_str(&relayer.address)
131 .map_err(|e| ApiError::BadRequest(format!("Invalid relayer address: {e}")))?;
132 if instructions.is_empty() {
133 return Err(ApiError::BadRequest(
134 "Instructions cannot be empty".to_string(),
135 ));
136 }
137
138 if instructions.len() > REQUEST_MAX_INSTRUCTIONS {
139 return Err(ApiError::BadRequest(format!(
140 "Too many instructions: {} exceeds maximum of {}",
141 instructions.len(),
142 REQUEST_MAX_INSTRUCTIONS
143 )));
144 }
145
146 let mut unique_accounts = HashSet::new();
147
148 for (idx, instruction) in instructions.iter().enumerate() {
149 let trimmed_program_id = instruction.program_id.trim();
151 if trimmed_program_id.is_empty() {
152 return Err(ApiError::BadRequest(format!(
153 "Instruction {idx}: program_id cannot be empty"
154 )));
155 }
156
157 let program_pubkey = Pubkey::from_str(trimmed_program_id).map_err(|e| {
159 ApiError::BadRequest(format!(
160 "Instruction {idx}: Invalid program_id '{trimmed_program_id}' - {e}"
161 ))
162 })?;
163
164 if program_pubkey == Pubkey::default() {
166 return Err(ApiError::BadRequest(format!(
167 "Instruction {idx}: program_id cannot be default pubkey"
168 )));
169 }
170
171 unique_accounts.insert(program_pubkey);
172
173 if instruction.accounts.len() > REQUEST_MAX_ACCOUNTS_PER_INSTRUCTION {
175 return Err(ApiError::BadRequest(format!(
176 "Instruction {}: Too many accounts {} exceeds maximum of {}",
177 idx,
178 instruction.accounts.len(),
179 REQUEST_MAX_ACCOUNTS_PER_INSTRUCTION
180 )));
181 }
182
183 for (acc_idx, account) in instruction.accounts.iter().enumerate() {
185 let trimmed_pubkey = account.pubkey.trim();
186 if trimmed_pubkey.is_empty() {
187 return Err(ApiError::BadRequest(format!(
188 "Instruction {idx} account {acc_idx}: pubkey cannot be empty"
189 )));
190 }
191
192 let pubkey = Pubkey::from_str(trimmed_pubkey).map_err(|e| {
193 ApiError::BadRequest(format!(
194 "Instruction {idx} account {acc_idx}: Invalid pubkey '{trimmed_pubkey}' - {e}"
195 ))
196 })?;
197
198 if account.is_signer && pubkey != relayer_pubkey {
200 return Err(ApiError::BadRequest(format!(
201 "Instruction {idx} account {acc_idx}: Only the relayer address {relayer_pubkey} can be marked as \
202 a signer, but '{pubkey}' is marked as a signer. The relayer can only provide \
203 its own signature."
204 )));
205 }
206
207 unique_accounts.insert(pubkey);
208 }
209
210 let decoded_data = base64_decode(&instruction.data).map_err(|e| {
212 ApiError::BadRequest(format!("Instruction {idx}: Invalid base64 data - {e}"))
213 })?;
214
215 if decoded_data.len() > REQUEST_MAX_INSTRUCTION_DATA_SIZE {
217 return Err(ApiError::BadRequest(format!(
218 "Instruction {}: Data too large ({} bytes, max: {} bytes)",
219 idx,
220 decoded_data.len(),
221 REQUEST_MAX_INSTRUCTION_DATA_SIZE
222 )));
223 }
224 }
225
226 if unique_accounts.len() > REQUEST_MAX_TOTAL_ACCOUNTS {
228 return Err(ApiError::BadRequest(format!(
229 "Too many unique accounts: {} exceeds Solana's limit of {}",
230 unique_accounts.len(),
231 REQUEST_MAX_TOTAL_ACCOUNTS
232 )));
233 }
234
235 Ok(())
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::models::RelayerRepoModel;
243 use base64::Engine;
244 use solana_sdk::{message::Message, pubkey::Pubkey};
245 use solana_system_interface::instruction as system_instruction;
246
247 fn create_test_relayer() -> RelayerRepoModel {
248 RelayerRepoModel {
249 id: "test-relayer".to_string(),
250 name: "Test Relayer".to_string(),
251 network: "solana".to_string(),
252 paused: false,
253 network_type: crate::models::NetworkType::Solana,
254 signer_id: "test-signer".to_string(),
255 policies: crate::models::RelayerNetworkPolicy::Solana(
256 crate::models::RelayerSolanaPolicy::default(),
257 ),
258 address: "6eoxMcGNaSRKcd8s84ukZjRZBJ27C5DrSXGH6nz73W8h".to_string(),
259 notification_id: None,
260 system_disabled: false,
261 disabled_reason: None,
262 custom_rpc_urls: None,
263 }
264 }
265
266 fn create_valid_instruction_spec() -> SolanaInstructionSpec {
267 SolanaInstructionSpec {
268 program_id: "11111111111111111111111111111112".to_string(), accounts: vec![
270 crate::models::SolanaAccountMeta {
271 pubkey: "6eoxMcGNaSRKcd8s84ukZjRZBJ27C5DrSXGH6nz73W8h".to_string(),
272 is_signer: true,
273 is_writable: true,
274 },
275 crate::models::SolanaAccountMeta {
276 pubkey: "HmZhRVuT8UuMrUJr1JsWFXTQU4EzwGVmQ29Q6QmzLbNs".to_string(),
277 is_signer: false,
278 is_writable: true,
279 },
280 ],
281 data: base64::prelude::BASE64_STANDARD.encode(
282 [2, 0, 0, 0]
283 .iter()
284 .chain(&1000000u64.to_le_bytes())
285 .chain(&[0, 0, 0, 0, 0, 0, 0])
286 .cloned()
287 .collect::<Vec<u8>>(),
288 ), }
290 }
291
292 fn create_valid_transaction(relayer_pubkey: &Pubkey) -> EncodedSerializedTransaction {
293 let recipient = Pubkey::new_unique();
294 let instruction = system_instruction::transfer(relayer_pubkey, &recipient, 1000000);
295 let message = Message::new(&[instruction], Some(relayer_pubkey));
296 let tx = solana_sdk::transaction::Transaction::new_unsigned(message);
297 let serialized = bincode::serialize(&tx).unwrap();
298 EncodedSerializedTransaction::new(base64::prelude::BASE64_STANDARD.encode(serialized))
299 }
300
301 fn create_transaction_with_wrong_fee_payer() -> EncodedSerializedTransaction {
302 let wrong_fee_payer = Pubkey::new_unique();
303 let recipient = Pubkey::new_unique();
304 let instruction = system_instruction::transfer(&wrong_fee_payer, &recipient, 1000000);
305 let message = Message::new(&[instruction], Some(&wrong_fee_payer));
306 let tx = solana_sdk::transaction::Transaction::new_unsigned(message);
307 let serialized = bincode::serialize(&tx).unwrap();
308 EncodedSerializedTransaction::new(base64::prelude::BASE64_STANDARD.encode(serialized))
309 }
310
311 #[test]
312 fn test_validate_valid_request_with_transaction() {
313 let relayer = create_test_relayer();
314 let relayer_pubkey = Pubkey::from_str(&relayer.address).unwrap();
315 let transaction = create_valid_transaction(&relayer_pubkey);
316
317 let request = SolanaTransactionRequest {
318 transaction: Some(transaction),
319 instructions: None,
320 valid_until: None,
321 };
322
323 assert!(request.validate(&relayer).is_ok());
324 }
325
326 #[test]
327 fn test_validate_valid_request_with_instructions() {
328 let relayer = create_test_relayer();
329 let instruction = create_valid_instruction_spec();
330
331 let request = SolanaTransactionRequest {
332 transaction: None,
333 instructions: Some(vec![instruction]),
334 valid_until: None,
335 };
336
337 assert!(request.validate(&relayer).is_ok());
338 }
339
340 #[test]
341 fn test_validate_invalid_both_transaction_and_instructions() {
342 let relayer = create_test_relayer();
343 let relayer_pubkey = Pubkey::from_str(&relayer.address).unwrap();
344 let transaction = create_valid_transaction(&relayer_pubkey);
345 let instruction = create_valid_instruction_spec();
346
347 let request = SolanaTransactionRequest {
348 transaction: Some(transaction),
349 instructions: Some(vec![instruction]),
350 valid_until: None,
351 };
352
353 let result = request.validate(&relayer);
354 assert!(result.is_err());
355 assert!(result
356 .unwrap_err()
357 .to_string()
358 .contains("Cannot provide both transaction and instructions"));
359 }
360
361 #[test]
362 fn test_validate_invalid_neither_transaction_nor_instructions() {
363 let relayer = create_test_relayer();
364
365 let request = SolanaTransactionRequest {
366 transaction: None,
367 instructions: None,
368 valid_until: None,
369 };
370
371 let result = request.validate(&relayer);
372 assert!(result.is_err());
373 assert!(result
374 .unwrap_err()
375 .to_string()
376 .contains("Must provide either transaction or instructions"));
377 }
378
379 #[test]
380 fn test_validate_valid_request_with_future_valid_until() {
381 let relayer = create_test_relayer();
382 let future_time = chrono::Utc::now() + chrono::Duration::hours(1);
383
384 let request = SolanaTransactionRequest {
385 transaction: None,
386 instructions: Some(vec![create_valid_instruction_spec()]),
387 valid_until: Some(future_time.to_rfc3339()),
388 };
389
390 assert!(request.validate(&relayer).is_ok());
391 }
392
393 #[test]
394 fn test_validate_invalid_past_valid_until() {
395 let relayer = create_test_relayer();
396 let past_time = chrono::Utc::now() - chrono::Duration::hours(1);
397
398 let request = SolanaTransactionRequest {
399 transaction: None,
400 instructions: Some(vec![create_valid_instruction_spec()]),
401 valid_until: Some(past_time.to_rfc3339()),
402 };
403
404 let result = request.validate(&relayer);
405 assert!(result.is_err());
406 assert!(result
407 .unwrap_err()
408 .to_string()
409 .contains("valid_until cannot be in the past"));
410 }
411
412 #[test]
413 fn test_validate_invalid_malformed_valid_until() {
414 let relayer = create_test_relayer();
415
416 let request = SolanaTransactionRequest {
417 transaction: None,
418 instructions: Some(vec![create_valid_instruction_spec()]),
419 valid_until: Some("invalid-timestamp".to_string()),
420 };
421
422 let result = request.validate(&relayer);
423 assert!(result.is_err());
424 assert!(result
425 .unwrap_err()
426 .to_string()
427 .contains("valid_until must be a valid RFC3339 timestamp"));
428 }
429
430 #[test]
431 fn test_validate_transaction_invalid_base64() {
432 let relayer = create_test_relayer();
433 let transaction = EncodedSerializedTransaction::new("invalid-base64!".to_string());
434
435 let result = SolanaTransactionRequest::validate_transaction(&transaction, &relayer);
436 assert!(result.is_err());
437 assert!(result
438 .unwrap_err()
439 .to_string()
440 .contains("Failed to decode transaction"));
441 }
442
443 #[test]
444 fn test_validate_transaction_wrong_fee_payer() {
445 let relayer = create_test_relayer();
446 let transaction = create_transaction_with_wrong_fee_payer();
447
448 let result = SolanaTransactionRequest::validate_transaction(&transaction, &relayer);
449 assert!(result.is_err());
450 assert!(result
451 .unwrap_err()
452 .to_string()
453 .contains("does not match relayer address"));
454 }
455
456 #[test]
457 fn test_validate_instructions_empty_instructions() {
458 let relayer = create_test_relayer();
459
460 let result = SolanaTransactionRequest::validate_instructions(&[], &relayer);
461 assert!(result.is_err());
462 assert!(result
463 .unwrap_err()
464 .to_string()
465 .contains("Instructions cannot be empty"));
466 }
467
468 #[test]
469 fn test_validate_instructions_too_many_instructions() {
470 let relayer = create_test_relayer();
471 let instructions = vec![create_valid_instruction_spec(); REQUEST_MAX_INSTRUCTIONS + 1];
472
473 let result = SolanaTransactionRequest::validate_instructions(&instructions, &relayer);
474 assert!(result.is_err());
475 assert!(result
476 .unwrap_err()
477 .to_string()
478 .contains("Too many instructions"));
479 }
480
481 #[test]
482 fn test_validate_instructions_empty_program_id() {
483 let relayer = create_test_relayer();
484 let mut instruction = create_valid_instruction_spec();
485 instruction.program_id = "".to_string();
486
487 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
488 assert!(result.is_err());
489 assert!(result
490 .unwrap_err()
491 .to_string()
492 .contains("program_id cannot be empty"));
493 }
494
495 #[test]
496 fn test_validate_instructions_invalid_program_id() {
497 let relayer = create_test_relayer();
498 let mut instruction = create_valid_instruction_spec();
499 instruction.program_id = "invalid-pubkey".to_string();
500
501 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
502 assert!(result.is_err());
503 assert!(result
504 .unwrap_err()
505 .to_string()
506 .contains("Invalid program_id"));
507 }
508
509 #[test]
510 fn test_validate_instructions_default_program_id() {
511 let relayer = create_test_relayer();
512 let mut instruction = create_valid_instruction_spec();
513 instruction.program_id = "11111111111111111111111111111111".to_string(); let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
516 assert!(result.is_err());
517 assert!(result
518 .unwrap_err()
519 .to_string()
520 .contains("program_id cannot be default pubkey"));
521 }
522
523 #[test]
524 fn test_validate_instructions_too_many_accounts_per_instruction() {
525 let relayer = create_test_relayer();
526 let mut instruction = create_valid_instruction_spec();
527 instruction.accounts =
528 vec![instruction.accounts[0].clone(); REQUEST_MAX_ACCOUNTS_PER_INSTRUCTION + 1];
529
530 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
531 assert!(result.is_err());
532 assert!(result
533 .unwrap_err()
534 .to_string()
535 .contains("Too many accounts"));
536 }
537
538 #[test]
539 fn test_validate_instructions_empty_account_pubkey() {
540 let relayer = create_test_relayer();
541 let mut instruction = create_valid_instruction_spec();
542 instruction.accounts[0].pubkey = "".to_string();
543
544 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
545 assert!(result.is_err());
546 assert!(result
547 .unwrap_err()
548 .to_string()
549 .contains("pubkey cannot be empty"));
550 }
551
552 #[test]
553 fn test_validate_instructions_invalid_account_pubkey() {
554 let relayer = create_test_relayer();
555 let mut instruction = create_valid_instruction_spec();
556 instruction.accounts[0].pubkey = "invalid-pubkey".to_string();
557
558 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
559 assert!(result.is_err());
560 assert!(result.unwrap_err().to_string().contains("Invalid pubkey"));
561 }
562
563 #[test]
564 fn test_validate_instructions_non_relayer_signer() {
565 let relayer = create_test_relayer();
566 let mut instruction = create_valid_instruction_spec();
567 instruction.accounts[1].is_signer = true;
569
570 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
571 assert!(result.is_err());
572 assert!(result
573 .unwrap_err()
574 .to_string()
575 .contains("Only the relayer address"));
576 }
577
578 #[test]
579 fn test_validate_instructions_invalid_base64_data() {
580 let relayer = create_test_relayer();
581 let mut instruction = create_valid_instruction_spec();
582 instruction.data = "invalid-base64!".to_string();
583
584 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
585 assert!(result.is_err());
586 assert!(result
587 .unwrap_err()
588 .to_string()
589 .contains("Invalid base64 data"));
590 }
591
592 #[test]
593 fn test_validate_instructions_data_too_large() {
594 let relayer = create_test_relayer();
595 let mut instruction = create_valid_instruction_spec();
596 let large_data = vec![0u8; REQUEST_MAX_INSTRUCTION_DATA_SIZE + 1];
598 instruction.data = base64::prelude::BASE64_STANDARD.encode(large_data);
599
600 let result = SolanaTransactionRequest::validate_instructions(&[instruction], &relayer);
601 assert!(result.is_err());
602 assert!(result.unwrap_err().to_string().contains("Data too large"));
603 }
604
605 #[test]
606 fn test_validate_instructions_too_many_unique_accounts() {
607 let relayer = create_test_relayer();
608 let mut instructions = Vec::new();
609
610 for i in 0..(REQUEST_MAX_TOTAL_ACCOUNTS + 1) {
612 let mut instruction = create_valid_instruction_spec();
613 instruction.program_id = format!("{:0>44}", i); instruction.accounts.push(crate::models::SolanaAccountMeta {
617 pubkey: format!("{:0>44}", i + 1000), is_signer: false,
619 is_writable: false,
620 });
621 instructions.push(instruction);
622 }
623
624 let mut instructions = Vec::new();
626 for _ in 0..10 {
627 instructions.push(create_valid_instruction_spec());
628 }
629
630 assert!(SolanaTransactionRequest::validate_instructions(&instructions, &relayer).is_ok());
632 }
633
634 #[test]
635 fn test_validate_instructions_too_many_unique_accounts_failure() {
636 let relayer = create_test_relayer();
637 let relayer_pubkey = Pubkey::from_str(&relayer.address).unwrap();
638 let mut instructions = Vec::new();
639 for _i in 0..(REQUEST_MAX_TOTAL_ACCOUNTS) {
641 let unique_account = Pubkey::new_unique();
644
645 let unique_program_id = Pubkey::new_unique();
647
648 instructions.push(SolanaInstructionSpec {
649 program_id: unique_program_id.to_string(),
651 accounts: vec![
652 crate::models::SolanaAccountMeta {
653 pubkey: relayer_pubkey.to_string(),
655 is_signer: true,
656 is_writable: true,
657 },
658 crate::models::SolanaAccountMeta {
660 pubkey: unique_account.to_string(),
661 is_signer: false,
662 is_writable: true,
663 },
664 ],
665 data: base64::prelude::BASE64_STANDARD.encode(vec![0u8]),
666 });
667 }
668
669 let result = SolanaTransactionRequest::validate_instructions(&instructions, &relayer);
676 assert!(result.is_err());
677 assert!(result
678 .unwrap_err()
679 .to_string()
680 .contains("Too many unique accounts"));
681 }
682}