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