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