openzeppelin_relayer/models/transaction/request/
solana.rs

1use 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    /// Pre-built base64-encoded transaction (mutually exclusive with instructions)
17    #[schema(nullable = true)]
18    pub transaction: Option<EncodedSerializedTransaction>,
19
20    /// Instructions to build transaction from (mutually exclusive with transaction)
21    #[schema(nullable = true)]
22    pub instructions: Option<Vec<SolanaInstructionSpec>>,
23
24    /// Optional RFC3339 timestamp when transaction should expire
25    #[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        // Validate pre-built transaction if provided
53        if let Some(ref transaction) = self.transaction {
54            Self::validate_transaction(transaction, relayer)?;
55        }
56
57        // Validate instructions if provided
58        if let Some(ref instructions) = self.instructions {
59            Self::validate_instructions(instructions, relayer)?;
60        }
61
62        // Validate valid_until if provided
63        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    /// Validates a pre-built Solana transaction
84    ///
85    /// Ensures the transaction meets requirements:
86    /// - Transaction can be decoded from base64
87    /// - Fee payer (source) matches the relayer address
88    fn validate_transaction(
89        transaction: &EncodedSerializedTransaction,
90        relayer: &RelayerRepoModel,
91    ) -> Result<(), ApiError> {
92        // Parse the transaction from encoded bytes
93        let tx = Transaction::try_from(transaction.clone())
94            .map_err(|e| ApiError::BadRequest(format!("Failed to decode transaction: {e}")))?;
95
96        // Get the fee payer (first account in account_keys)
97        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        // Parse relayer address
102        let relayer_pubkey = Pubkey::from_str(&relayer.address)
103            .map_err(|e| ApiError::BadRequest(format!("Invalid relayer address: {e}")))?;
104
105        // Validate fee payer matches relayer address
106        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    /// Validates Solana instruction specifications
116    ///
117    /// Ensures all instruction fields are valid:
118    /// - Number of instructions within reasonable limits (max 64)
119    /// - Program IDs are valid Solana public keys (not empty, not default pubkey)
120    /// - Account public keys are valid (not empty)
121    /// - Accounts per instruction within Solana's limit (max 64)
122    /// - Total unique accounts don't exceed Solana's limit (max 64)
123    /// - Instruction data is valid base64 and within size limits (max 1000 bytes)
124    /// - Only the relayer can be marked as a signer (relayer auto-signs as fee payer)
125    fn validate_instructions(
126        instructions: &[SolanaInstructionSpec],
127        relayer: &RelayerRepoModel,
128    ) -> Result<(), ApiError> {
129        // Parse relayer address once for validation
130        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            // Validate program_id is not empty/whitespace
150            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            // Validate program_id is valid pubkey
158            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            // Reject default/zero pubkey as program_id
165            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            // Validate number of accounts per instruction
174            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            // Validate account pubkeys
184            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                // Validate that only the relayer can be marked as a signer
199                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            // Validate data is valid base64 and decode it
211            let decoded_data = base64_decode(&instruction.data).map_err(|e| {
212                ApiError::BadRequest(format!("Instruction {idx}: Invalid base64 data - {e}"))
213            })?;
214
215            // Validate decoded data size
216            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        // Validate total unique accounts across all instructions
227        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(), // System program
269            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            ), // Transfer 1 SOL
289        }
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(); // Default pubkey
514
515        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        // Make the second account (which is not the relayer) a signer
568        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        // Create data larger than REQUEST_MAX_INSTRUCTION_DATA_SIZE
597        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        // Create instructions that will exceed REQUEST_MAX_TOTAL_ACCOUNTS unique accounts
611        for i in 0..(REQUEST_MAX_TOTAL_ACCOUNTS + 1) {
612            let mut instruction = create_valid_instruction_spec();
613            // Change program_id to create unique accounts
614            instruction.program_id = format!("{:0>44}", i); // Create unique but invalid pubkeys
615                                                            // Add a unique account
616            instruction.accounts.push(crate::models::SolanaAccountMeta {
617                pubkey: format!("{:0>44}", i + 1000), // Another unique account
618                is_signer: false,
619                is_writable: false,
620            });
621            instructions.push(instruction);
622        }
623
624        // Create multiple instructions with different valid pubkeys
625        let mut instructions = Vec::new();
626        for _ in 0..10 {
627            instructions.push(create_valid_instruction_spec());
628        }
629
630        // This should pass since we're using the same accounts repeatedly
631        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        // We will generate REQUEST_MAX_TOTAL_ACCOUNTS + 1 unique accounts total.
640        for _i in 0..(REQUEST_MAX_TOTAL_ACCOUNTS) {
641            // We need to go up to the limit + 1
642            // Create a unique non-relayer pubkey for the instruction
643            let unique_account = Pubkey::new_unique();
644
645            // This program ID is guaranteed to be unique and valid
646            let unique_program_id = Pubkey::new_unique();
647
648            instructions.push(SolanaInstructionSpec {
649                // Unique program ID consumes a unique account slot
650                program_id: unique_program_id.to_string(),
651                accounts: vec![
652                    crate::models::SolanaAccountMeta {
653                        // The relayer's key is always included (1 unique key)
654                        pubkey: relayer_pubkey.to_string(),
655                        is_signer: true,
656                        is_writable: true,
657                    },
658                    // Unique account key consumes another unique account slot
659                    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        // Final instruction to push it over the limit of REQUEST_MAX_TOTAL_ACCOUNTS (e.g., 64)
670        // If REQUEST_MAX_TOTAL_ACCOUNTS=64, the loop created 64 instructions,
671        // with 1 relayer key + 63 other unique keys, for a total of 1 + 63*2 unique keys,
672        // which vastly exceeds the limit.
673        // We only need to check that the limit is hit and failed.
674
675        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}