Skip to main content

anchor_litesvm/
program.rs

1//! Simplified instruction builder for LiteSVM testing without RPC overhead.
2//!
3//! This module provides a clean, testing-focused API that removes unnecessary
4//! RPC-layer abstractions like `.request()` and `.remove(0)`.
5
6use anchor_lang::{InstructionData, ToAccountMetas};
7use solana_program::{instruction::Instruction, pubkey::Pubkey};
8
9/// A lightweight Program wrapper for building instructions in tests.
10///
11/// Simplified API for testing without RPC layer abstractions:
12/// ```ignore
13/// let ix = ctx.program()
14///     .accounts(my_program::accounts::Transfer { ... })
15///     .args(my_program::instruction::Transfer { ... })
16///     .instruction()?;
17/// ```
18#[derive(Copy, Clone)]
19pub struct Program {
20    program_id: Pubkey,
21}
22
23impl Program {
24    /// Create a new Program instance for the given program ID
25    pub fn new(program_id: Pubkey) -> Self {
26        Self { program_id }
27    }
28
29    /// Start building an instruction with accounts.
30    ///
31    /// This returns an `InstructionBuilder` that you can chain with `.args()` and `.instruction()`.
32    ///
33    /// # Example
34    /// ```ignore
35    /// let ix = ctx.program()
36    ///     .accounts(my_program::accounts::Initialize {
37    ///         user: user.pubkey(),
38    ///         account: data_account,
39    ///         system_program: system_program::id(),
40    ///     })
41    ///     .args(my_program::instruction::Initialize { value: 42 })
42    ///     .instruction()?;
43    /// ```
44    pub fn accounts<T: ToAccountMetas>(self, accounts: T) -> InstructionBuilder {
45        InstructionBuilder {
46            program_id: self.program_id,
47            accounts: accounts.to_account_metas(None),
48            data: Vec::new(),
49        }
50    }
51
52    /// Get the program ID
53    pub fn id(&self) -> Pubkey {
54        self.program_id
55    }
56}
57
58/// Builder for constructing instructions in a fluent, chainable manner.
59///
60/// You typically don't create this directly - use `program().accounts()` instead.
61pub struct InstructionBuilder {
62    program_id: Pubkey,
63    accounts: Vec<solana_program::instruction::AccountMeta>,
64    data: Vec<u8>,
65}
66
67impl InstructionBuilder {
68    /// Set the instruction arguments
69    ///
70    /// # Example
71    /// ```ignore
72    /// .args(my_program::instruction::Transfer { amount: 1000 })
73    /// ```
74    pub fn args<T: InstructionData>(mut self, args: T) -> Self {
75        self.data = args.data();
76        self
77    }
78
79    /// Build and return the instruction.
80    ///
81    /// This is the final method in the chain that produces the `Instruction`.
82    ///
83    /// # Example
84    /// ```ignore
85    /// let ix = ctx.program()
86    ///     .accounts(...)
87    ///     .args(...)
88    ///     .instruction()?;
89    /// ```
90    pub fn instruction(self) -> Result<Instruction, Box<dyn std::error::Error>> {
91        if self.data.is_empty() {
92            return Err("No instruction data provided. Call .args() before .instruction()".into());
93        }
94
95        Ok(Instruction {
96            program_id: self.program_id,
97            accounts: self.accounts,
98            data: self.data,
99        })
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::Program;
106    use anchor_lang::{prelude::*, InstructionData, ToAccountMetas};
107    use solana_program::instruction::AccountMeta;
108    use solana_program::pubkey::Pubkey;
109
110    struct TestAccounts {
111        user: Pubkey,
112        account: Pubkey,
113    }
114
115    impl ToAccountMetas for TestAccounts {
116        fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
117            vec![
118                AccountMeta::new(self.user, true),
119                AccountMeta::new(self.account, false),
120            ]
121        }
122    }
123
124    #[derive(AnchorSerialize, AnchorDeserialize)]
125    struct TestArgs {
126        amount: u64,
127    }
128
129    impl anchor_lang::Discriminator for TestArgs {
130        const DISCRIMINATOR: &'static [u8] = &[1, 2, 3, 4, 5, 6, 7, 8];
131    }
132
133    impl InstructionData for TestArgs {
134        fn data(&self) -> Vec<u8> {
135            let mut data = Vec::new();
136            data.extend_from_slice(Self::DISCRIMINATOR);
137            self.serialize(&mut data).unwrap();
138            data
139        }
140    }
141
142    #[test]
143    fn test_simplified_syntax() {
144        let program_id = Pubkey::new_unique();
145        let user = Pubkey::new_unique();
146        let account = Pubkey::new_unique();
147
148        // New simplified syntax for testing
149        let program = Program::new(program_id);
150        let ix = program
151            .accounts(TestAccounts { user, account })
152            .args(TestArgs { amount: 100 })
153            .instruction()
154            .unwrap();
155
156        assert_eq!(ix.program_id, program_id);
157        assert_eq!(ix.accounts.len(), 2);
158        assert!(ix.data.len() > 8);
159    }
160}