1use solana_instruction::{AccountMeta, Instruction};
15use solana_program_error::ProgramError;
16use solana_pubkey::Pubkey;
17
18use super::InstructionInputParsing;
19pub use crate::instruction::create_buffer::SPL_TOKEN_PROGRAM_ID;
20use crate::SettlementInstruction;
21
22pub struct ReclaimBuffer<'a> {
36 pub program_id: Pubkey,
37 pub state_pda: Pubkey,
38 pub reclaim_authority: Pubkey,
39 pub reclaim_recipient: Pubkey,
40 pub buffers: &'a [(Pubkey, Pubkey)],
41}
42
43impl From<ReclaimBuffer<'_>> for Instruction {
44 fn from(builder: ReclaimBuffer<'_>) -> Self {
45 let mut accounts = vec![
46 AccountMeta::new_readonly(builder.state_pda, false),
47 AccountMeta::new_readonly(builder.reclaim_authority, true),
48 AccountMeta::new(builder.reclaim_recipient, false),
49 AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
50 ];
51 for (buffer_pda, mint) in builder.buffers {
52 accounts.push(AccountMeta::new(*buffer_pda, false));
53 accounts.push(AccountMeta::new_readonly(*mint, false));
54 }
55 Instruction {
56 program_id: builder.program_id,
57 accounts,
58 data: vec![SettlementInstruction::ReclaimBuffer.discriminator()],
59 }
60 }
61}
62
63pub struct ReclaimBufferInput<'a, A> {
65 pub state_pda: &'a A,
66 pub reclaim_authority: &'a A,
67 pub reclaim_recipient: &'a A,
68 pub token_program: &'a A,
69 pub buffers: &'a [[A; 2]],
71}
72
73impl<'a, A> InstructionInputParsing<'a, A> for ReclaimBufferInput<'a, A> {
74 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::ReclaimBuffer;
75
76 fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
77 if !instruction_data.is_empty() {
78 return Err(ProgramError::InvalidInstructionData);
79 }
80 let [state_pda, reclaim_authority, reclaim_recipient, token_program, rest @ ..] = accounts
85 else {
86 return Err(ProgramError::NotEnoughAccountKeys);
87 };
88 let (buffers, remainder) = rest.as_chunks::<2>();
93 if !remainder.is_empty() || buffers.is_empty() {
94 return Err(ProgramError::NotEnoughAccountKeys);
95 }
96
97 Ok(Self {
98 state_pda,
99 reclaim_authority,
100 reclaim_recipient,
101 token_program,
102 buffers,
103 })
104 }
105}
106
107#[cfg(any(test, feature = "test-fixtures"))]
110pub mod fixtures {
111 use solana_address::Address;
112
113 use super::{Instruction, ReclaimBuffer};
114
115 pub const NUM_SHARED_ACCOUNTS: usize = 4;
119
120 pub fn reclaim_buffer_data() -> Vec<u8> {
123 let zero = Address::new_from_array([0; 32]);
124 Instruction::from(ReclaimBuffer {
125 program_id: zero,
126 state_pda: zero,
127 reclaim_authority: zero,
128 reclaim_recipient: zero,
129 buffers: &[(zero, zero)],
130 })
131 .data
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::fixtures::{reclaim_buffer_data, NUM_SHARED_ACCOUNTS};
138 use super::*;
139 use crate::fixtures::pubkey_from_seed;
140 use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
141 use crate::instruction::tests::{
142 assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
143 };
144
145 #[test]
146 fn reclaim_buffer_input_parses_valid_input() {
147 let program_id = pubkey_from_seed("program id");
148 let state_pda = pubkey_from_seed("state pda");
149 let reclaim_authority = pubkey_from_seed("reclaim authority");
150 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
151 let buffer_pda = pubkey_from_seed("buffer pda");
152 let mint = pubkey_from_seed("mint");
153
154 let data = Instruction::from(ReclaimBuffer {
155 program_id,
156 state_pda,
157 reclaim_authority,
158 reclaim_recipient,
159 buffers: &[(buffer_pda, mint)],
160 })
161 .data;
162 let token_program = pubkey_from_seed("token program");
163 let accounts = [
164 fake_account(state_pda),
165 fake_account(reclaim_authority),
166 fake_account(reclaim_recipient),
167 fake_account(token_program),
168 fake_account(buffer_pda),
169 fake_account(mint),
170 ];
171
172 let ReclaimBufferInput {
173 state_pda: parsed_state_pda,
174 reclaim_authority: parsed_reclaim_authority,
175 reclaim_recipient: parsed_reclaim_recipient,
176 token_program: parsed_token_program,
177 buffers,
178 } = ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
179
180 assert_eq!(*parsed_state_pda.address(), state_pda);
181 assert_eq!(*parsed_reclaim_authority.address(), reclaim_authority);
182 assert_eq!(*parsed_reclaim_recipient.address(), reclaim_recipient);
183 assert_eq!(*parsed_token_program.address(), token_program);
184 assert_eq!(buffers.len(), 1, "one buffer is one pair");
185 assert_eq!(*buffers[0][0].address(), buffer_pda);
186 assert_eq!(*buffers[0][1].address(), mint);
187 }
188
189 #[test]
190 fn reclaim_buffer_input_parses_multiple_buffers() {
191 let program_id = pubkey_from_seed("program id");
192 let state_pda = pubkey_from_seed("state pda");
193 let reclaim_authority = pubkey_from_seed("reclaim authority");
194 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
195 let token_program = pubkey_from_seed("token program");
196 let buffer_a = pubkey_from_seed("buffer a");
197 let mint_a = pubkey_from_seed("mint a");
198 let buffer_b = pubkey_from_seed("buffer b");
199 let mint_b = pubkey_from_seed("mint b");
200
201 let data = Instruction::from(ReclaimBuffer {
202 program_id,
203 state_pda,
204 reclaim_authority,
205 reclaim_recipient,
206 buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
207 })
208 .data;
209 let accounts = [
210 fake_account(state_pda),
211 fake_account(reclaim_authority),
212 fake_account(reclaim_recipient),
213 fake_account(token_program),
214 fake_account(buffer_a),
215 fake_account(mint_a),
216 fake_account(buffer_b),
217 fake_account(mint_b),
218 ];
219
220 let ReclaimBufferInput { buffers, .. } =
221 ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
222
223 assert_eq!(buffers.len(), 2, "two buffers are two pairs");
224 assert_eq!(
225 buffers[0].each_ref().map(|a| *a.address()),
226 [buffer_a, mint_a]
227 );
228 assert_eq!(
229 buffers[1].each_ref().map(|a| *a.address()),
230 [buffer_b, mint_b]
231 );
232 }
233
234 #[test]
235 fn reclaim_buffer_input_rejects_zero_buffers() {
236 let data = reclaim_buffer_data();
237 let accounts = fake_sequential_accounts::<NUM_SHARED_ACCOUNTS>();
239 assert_eq!(
240 ReclaimBufferInput::parse(&data, &accounts).err(),
241 Some(ProgramError::NotEnoughAccountKeys),
242 "an instruction that reclaims no buffers is rejected",
243 );
244 }
245
246 #[test]
247 fn reclaim_buffer_input_rejects_long_data() {
248 let mut data = reclaim_buffer_data();
249 data.push(0); assert_eq!(
251 ReclaimBufferInput::parse(&data, &[0]).err(),
252 Some(ProgramError::InvalidInstructionData),
253 );
254 }
255
256 #[test]
257 fn reclaim_buffer_input_rejects_missing_accounts() {
258 let data = reclaim_buffer_data();
259 let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS - 1 }>();
261 assert_eq!(
262 ReclaimBufferInput::parse(&data, &accounts).err(),
263 Some(ProgramError::NotEnoughAccountKeys),
264 );
265 }
266
267 #[test]
268 fn reclaim_buffer_input_rejects_incomplete_pair() {
269 let data = reclaim_buffer_data();
270 let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + 3 }>();
273 assert_eq!(
274 ReclaimBufferInput::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 state_pda = pubkey_from_seed("state pda");
283 let reclaim_authority = pubkey_from_seed("reclaim authority");
284 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
285 let buffer_pda = pubkey_from_seed("buffer pda");
286 let mint = pubkey_from_seed("mint");
287 let Instruction { data, .. } = ReclaimBuffer {
288 program_id,
289 state_pda,
290 reclaim_authority,
291 reclaim_recipient,
292 buffers: &[(buffer_pda, mint)],
293 }
294 .into();
295 assert_eq!(
296 data,
297 vec![SettlementInstruction::ReclaimBuffer.discriminator()]
298 );
299 }
300
301 #[test]
302 fn single_buffer_has_expected_accounts() {
303 let program_id = pubkey_from_seed("program id");
304 let state_pda = pubkey_from_seed("state pda");
305 let reclaim_authority = pubkey_from_seed("reclaim authority");
306 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
307 let buffer_pda = pubkey_from_seed("buffer pda");
308 let mint = pubkey_from_seed("mint");
309 let Instruction { accounts, .. } = ReclaimBuffer {
310 program_id,
311 state_pda,
312 reclaim_authority,
313 reclaim_recipient,
314 buffers: &[(buffer_pda, mint)],
315 }
316 .into();
317
318 assert_eq!(accounts.len(), 6);
319 assert_readonly_nonsigner(&accounts[0], state_pda);
320 assert_readonly_signer(&accounts[1], reclaim_authority);
321 assert_writable_nonsigner(&accounts[2], reclaim_recipient);
322 assert_readonly_nonsigner(&accounts[3], SPL_TOKEN_PROGRAM_ID);
323 assert_writable_nonsigner(&accounts[4], buffer_pda);
324 assert_readonly_nonsigner(&accounts[5], mint);
325 }
326
327 #[test]
328 fn recipient_may_be_the_reclaim_authority_itself() {
329 let reclaim_authority = pubkey_from_seed("reclaim authority");
330 let Instruction { accounts, .. } = ReclaimBuffer {
331 program_id: pubkey_from_seed("program id"),
332 state_pda: pubkey_from_seed("state pda"),
333 reclaim_authority,
334 reclaim_recipient: reclaim_authority,
335 buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"))],
336 }
337 .into();
338
339 assert_readonly_signer(&accounts[1], reclaim_authority);
340 assert_writable_nonsigner(&accounts[2], reclaim_authority);
341 }
342
343 #[test]
344 fn multiple_buffers_append_pairs_after_shared_accounts() {
345 let program_id = pubkey_from_seed("program id");
346 let state_pda = pubkey_from_seed("state pda");
347 let reclaim_authority = pubkey_from_seed("reclaim authority");
348 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
349 let buffer_a = pubkey_from_seed("buffer a");
350 let mint_a = pubkey_from_seed("mint a");
351 let buffer_b = pubkey_from_seed("buffer b");
352 let mint_b = pubkey_from_seed("mint b");
353 let Instruction { accounts, .. } = ReclaimBuffer {
354 program_id,
355 state_pda,
356 reclaim_authority,
357 reclaim_recipient,
358 buffers: &[(buffer_a, mint_a), (buffer_b, mint_b)],
359 }
360 .into();
361
362 assert_eq!(accounts.len(), 4 + 2 * 2);
364 assert_writable_nonsigner(&accounts[4], buffer_a);
365 assert_readonly_nonsigner(&accounts[5], mint_a);
366 assert_writable_nonsigner(&accounts[6], buffer_b);
367 assert_readonly_nonsigner(&accounts[7], mint_b);
368 }
369
370 #[test]
371 fn empty_buffers_has_only_shared_accounts() {
372 let program_id = pubkey_from_seed("program id");
373 let state_pda = pubkey_from_seed("state pda");
374 let reclaim_authority = pubkey_from_seed("reclaim authority");
375 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
376 let Instruction { accounts, .. } = ReclaimBuffer {
377 program_id,
378 state_pda,
379 reclaim_authority,
380 reclaim_recipient,
381 buffers: &[],
382 }
383 .into();
384 assert_eq!(accounts.len(), NUM_SHARED_ACCOUNTS);
385 }
386}