Skip to main content

cow_settlement_interface/instruction/
create_buffer.rs

1//! `CreateBuffer` instruction builder.
2//!
3//! Allocates one or more per-token buffer PDAs (see [`crate::pda::buffer`]) as
4//! SPL token accounts and initializes each with the settlement state PDA as its
5//! token authority. Each token is identified by its `mint` account; the buffer
6//! address must be the canonical PDA for that mint.
7
8use solana_instruction::{AccountMeta, Instruction};
9use solana_program_error::ProgramError;
10use solana_pubkey::Pubkey;
11
12pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
13
14use super::InstructionInputParsing;
15use crate::SettlementInstruction;
16
17/// The SPL Token program. Buffers are created as token accounts owned by this
18/// program.
19pub use spl_token_interface::ID as SPL_TOKEN_PROGRAM_ID;
20
21/// Builder for a `CreateBuffer` instruction that creates one buffer per
22/// `(buffer_pda, mint)` pair in `buffers`.
23///
24/// Each `buffer_pda` must be the canonical PDA returned by
25/// [`crate::pda::buffer::find_buffer_pda`] for its `mint`; the program derives
26/// the bump itself and rejects any other address. `payer` funds every new token
27/// account's rent.
28///
29/// Buffer creation is idempotent: an already-existing `buffer_pda` is left
30/// unchanged and the instruction still succeeds, so two parties racing to
31/// create the same buffer both succeed.
32///
33/// Wire format: `[discriminator=4]`, 1 byte. The tokens are implied by the
34/// `mint` accounts, so no further data is needed.
35/// Required accounts:
36/// `[payer (W,S), system_program (R), token_program (R), (buffer_pda (W), mint (R))...]`.
37/// The three shared accounts come first and are read positionally; the
38/// per-buffer pairs follow. The system program only has to be present so the
39/// `CreateAccount` CPI can dispatch; it isn't read by index.
40pub struct CreateBuffers<'a> {
41    pub program_id: Pubkey,
42    pub payer: Pubkey,
43    pub buffers: &'a [(Pubkey, Pubkey)],
44}
45
46impl From<CreateBuffers<'_>> for Instruction {
47    fn from(builder: CreateBuffers<'_>) -> Self {
48        let mut accounts = vec![
49            AccountMeta::new(builder.payer, true),
50            AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
51            AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
52        ];
53        for (buffer_pda, mint) in builder.buffers {
54            accounts.push(AccountMeta::new(*buffer_pda, false));
55            accounts.push(AccountMeta::new_readonly(*mint, false));
56        }
57        Instruction {
58            program_id: builder.program_id,
59            accounts,
60            data: vec![SettlementInstruction::CreateBuffer.discriminator()],
61        }
62    }
63}
64
65/// The two accounts of one buffer to create.
66pub struct BufferAccounts<'a, A> {
67    pub buffer_pda: &'a A,
68    pub mint: &'a A,
69}
70
71/// Parsed inputs of a `CreateBuffer` instruction.
72pub struct CreateBufferInput<'a, A> {
73    pub payer: &'a A,
74    pub token_program: &'a A,
75    buffer_pairs: &'a [[A; 2]],
76}
77
78impl<'a, A> CreateBufferInput<'a, A> {
79    /// The buffers to create, one [`BufferAccounts`] per pair.
80    pub fn buffers(&self) -> impl Iterator<Item = BufferAccounts<'a, A>> + '_ {
81        self.buffer_pairs
82            .iter()
83            .map(|[buffer_pda, mint]| BufferAccounts { buffer_pda, mint })
84    }
85}
86
87impl<'a, A> InstructionInputParsing<'a, A> for CreateBufferInput<'a, A> {
88    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::CreateBuffer;
89
90    fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
91        if !instruction_data.is_empty() {
92            return Err(ProgramError::InvalidInstructionData);
93        }
94        // Accounts: [payer (W,S), system_program (R), token_program (R),
95        // (buffer_pda (W), mint (R))...]. The three shared accounts come first;
96        // the per-buffer pairs follow, one pair per buffer. The system program
97        // needs to be present for the `CreateAccount` CPI but isn't dereferenced
98        // here.
99        let [payer, _system, token_program, rest @ ..] = accounts else {
100            return Err(ProgramError::NotEnoughAccountKeys);
101        };
102        // Group the trailing accounts into `[buffer_pda, mint]` pairs. Each
103        // buffer needs both, so a stray odd account left over is a malformed
104        // instruction. There must be at least one pair: an instruction that
105        // creates no buffers is rejected as a likely encoding issue.
106        let rest: &'a [A] = rest;
107        let (buffers, remainder) = rest.as_chunks::<2>();
108        if !remainder.is_empty() || buffers.is_empty() {
109            return Err(ProgramError::NotEnoughAccountKeys);
110        }
111
112        Ok(Self {
113            payer,
114            token_program,
115            buffer_pairs: buffers,
116        })
117    }
118}
119
120/// Test scaffolding for `CreateBuffer` parsing and handling, shared by this
121/// crate's tests and the settlement program's via the `test-fixtures` feature.
122#[cfg(any(test, feature = "test-fixtures"))]
123pub mod fixtures {
124    use solana_address::Address;
125
126    use super::{CreateBuffers, Instruction};
127
128    /// Number of accounts that don't depend on the number of buffers created:
129    /// payer, system program, and token program.
130    pub const NUM_SHARED_ACCOUNTS: usize = 3;
131
132    /// `CreateBuffer` instruction data with placeholder addresses, for failure
133    /// cases where the input is irrelevant.
134    pub fn create_buffer_data() -> Vec<u8> {
135        let zero = Address::new_from_array([0; 32]);
136        Instruction::from(CreateBuffers {
137            program_id: zero,
138            payer: zero,
139            buffers: &[(zero, zero)],
140        })
141        .data
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::fixtures::{create_buffer_data, NUM_SHARED_ACCOUNTS};
148    use super::*;
149    use crate::instruction::fixtures::{
150        fake_account, fake_account_from_array, fake_sequential_accounts,
151    };
152    use crate::instruction::tests::{
153        assert_readonly_nonsigner, assert_writable_nonsigner, assert_writable_signer,
154    };
155    use solana_account_view::AccountView;
156    use solana_address::Address;
157
158    #[test]
159    fn create_buffer_input_parses_valid_input() {
160        let program_id: Address = Address::new_from_array([1; 32]);
161        let payer: Address = Address::new_from_array([2; 32]);
162        let system_program = fake_account_from_array([4; 32]);
163        let token_program = Address::new_from_array([3; 32]);
164        let buffer_pda = Address::new_from_array([5; 32]);
165        let mint = Address::new_from_array([6; 32]);
166
167        let data = Instruction::from(CreateBuffers {
168            program_id,
169            payer,
170            buffers: &[(buffer_pda, mint)],
171        })
172        .data;
173        let accounts = [
174            fake_account(payer),
175            system_program,
176            fake_account(token_program),
177            fake_account(buffer_pda),
178            fake_account(mint),
179        ];
180
181        let input = CreateBufferInput::parse(&data, &accounts).expect("parse should succeed");
182
183        assert_eq!(*input.payer.address(), payer);
184        assert_eq!(*input.token_program.address(), token_program);
185        let buffers: Vec<_> = input.buffers().collect();
186        assert_eq!(buffers.len(), 1, "one buffer is one (pda, mint) pair");
187        assert_eq!(*buffers[0].buffer_pda.address(), buffer_pda);
188        assert_eq!(*buffers[0].mint.address(), mint);
189    }
190
191    #[test]
192    fn create_buffer_input_parses_multiple_buffers() {
193        let program_id = Address::new_from_array([1; 32]);
194        let payer = Address::new_from_array([2; 32]);
195        let token_program = Address::new_from_array([3; 32]);
196        let buffer_a = Address::new_from_array([5; 32]);
197        let mint_a = Address::new_from_array([6; 32]);
198        let buffer_b = Address::new_from_array([7; 32]);
199        let mint_b = Address::new_from_array([8; 32]);
200
201        let data = Instruction::from(CreateBuffers {
202            program_id,
203            payer,
204            buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
205        })
206        .data;
207        let accounts = [
208            fake_account(payer),
209            fake_account_from_array([4; 32]),
210            fake_account(token_program),
211            fake_account(buffer_a),
212            fake_account(mint_a),
213            fake_account(buffer_b),
214            fake_account(mint_b),
215        ];
216
217        let input = CreateBufferInput::parse(&data, &accounts).expect("parse should succeed");
218
219        let buffers: Vec<_> = input.buffers().collect();
220        assert_eq!(
221            [*buffers[0].buffer_pda.address(), *buffers[0].mint.address()],
222            [buffer_a, mint_a]
223        );
224        assert_eq!(
225            [*buffers[1].buffer_pda.address(), *buffers[1].mint.address()],
226            [buffer_b, mint_b]
227        );
228    }
229
230    #[test]
231    fn create_buffer_input_rejects_zero_buffers() {
232        let data = vec![SettlementInstruction::CreateBuffer.discriminator()];
233        // Only the three shared accounts, no (pda, mint) pairs.
234        let accounts = fake_sequential_accounts::<NUM_SHARED_ACCOUNTS>();
235        assert_eq!(
236            CreateBufferInput::parse(&data, &accounts).err(),
237            Some(ProgramError::NotEnoughAccountKeys),
238            "an instruction that creates no buffers is rejected",
239        );
240    }
241
242    #[test]
243    fn create_buffer_input_rejects_long_data() {
244        let mut data = create_buffer_data();
245        data.push(0); // trailing byte
246        let accounts: [AccountView; 0] = [];
247        assert_eq!(
248            CreateBufferInput::parse(&data, &accounts).err(),
249            Some(ProgramError::InvalidInstructionData),
250        );
251    }
252
253    #[test]
254    fn create_buffer_input_rejects_missing_accounts() {
255        let data = create_buffer_data();
256        // Fewer than the three shared accounts.
257        let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS - 1 }>();
258        assert_eq!(
259            CreateBufferInput::parse(&data, &accounts).err(),
260            Some(ProgramError::NotEnoughAccountKeys),
261        );
262    }
263
264    #[test]
265    fn create_buffer_input_rejects_odd_pair_accounts() {
266        let data = create_buffer_data();
267        // Three shared accounts plus a dangling account that can't form a pair.
268        let accounts = fake_sequential_accounts::<4>();
269        assert_eq!(
270            CreateBufferInput::parse(&data, &accounts).err(),
271            Some(ProgramError::NotEnoughAccountKeys),
272        );
273    }
274
275    #[test]
276    fn instruction_data_has_expected_layout() {
277        let program_id = Pubkey::new_from_array([1; 32]);
278        let payer = Pubkey::new_from_array([2; 32]);
279        let buffer_pda = Pubkey::new_from_array([3; 32]);
280        let mint = Pubkey::new_from_array([4; 32]);
281        let Instruction { data, .. } = CreateBuffers {
282            program_id,
283            payer,
284            buffers: &[(buffer_pda, mint)],
285        }
286        .into();
287        assert_eq!(
288            data,
289            vec![SettlementInstruction::CreateBuffer.discriminator()]
290        );
291    }
292
293    #[test]
294    fn single_buffer_has_expected_accounts() {
295        let program_id = Pubkey::new_from_array([1; 32]);
296        let payer = Pubkey::new_from_array([2; 32]);
297        let buffer_pda = Pubkey::new_from_array([3; 32]);
298        let mint = Pubkey::new_from_array([4; 32]);
299        let Instruction { accounts, .. } = CreateBuffers {
300            program_id,
301            payer,
302            buffers: &[(buffer_pda, mint)],
303        }
304        .into();
305
306        assert_eq!(accounts.len(), 5);
307        // payer funds rent; the system and token programs and the mint are only
308        // referenced; buffer_pda is created (written) and the mint is
309        // untouched.
310        assert_writable_signer(&accounts[0], payer);
311        assert_readonly_nonsigner(&accounts[1], SYSTEM_PROGRAM_ID);
312        assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID);
313        assert_writable_nonsigner(&accounts[3], buffer_pda);
314        assert_readonly_nonsigner(&accounts[4], mint);
315    }
316
317    #[test]
318    fn multiple_buffers_append_pairs_after_shared_accounts() {
319        let program_id = Pubkey::new_from_array([1; 32]);
320        let payer = Pubkey::new_from_array([2; 32]);
321        let buffer_a = Pubkey::new_from_array([3; 32]);
322        let mint_a = Pubkey::new_from_array([4; 32]);
323        let buffer_b = Pubkey::new_from_array([5; 32]);
324        let mint_b = Pubkey::new_from_array([6; 32]);
325        let Instruction { accounts, .. } = CreateBuffers {
326            program_id,
327            payer,
328            buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
329        }
330        .into();
331
332        // Three shared accounts followed by two (buffer, mint) pairs.
333        assert_eq!(accounts.len(), 3 + 2 * 2);
334        assert_writable_nonsigner(&accounts[3], buffer_a);
335        assert_readonly_nonsigner(&accounts[4], mint_a);
336        assert_writable_nonsigner(&accounts[5], buffer_b);
337        assert_readonly_nonsigner(&accounts[6], mint_b);
338    }
339
340    #[test]
341    fn empty_buffers_has_only_shared_accounts() {
342        let program_id = Pubkey::new_from_array([1; 32]);
343        let payer = Pubkey::new_from_array([2; 32]);
344        let Instruction { accounts, .. } = CreateBuffers {
345            program_id,
346            payer,
347            buffers: &[],
348        }
349        .into();
350        assert_eq!(accounts.len(), 3);
351    }
352}