1use solana_instruction::{AccountMeta, Instruction};
25use solana_program_error::ProgramError;
26use solana_pubkey::Pubkey;
27
28use super::InstructionInputParsing;
29use crate::SettlementInstruction;
30
31pub const ACCOUNTS_PER_BUFFER: usize = 2;
34
35pub struct ReclaimBuffer<'a> {
52 pub program_id: Pubkey,
53 pub state_pda: Pubkey,
54 pub reclaim_authority: Pubkey,
55 pub reclaim_recipient: Pubkey,
56 pub token_program: Pubkey,
59 pub buffers: &'a [(Pubkey, Pubkey, u64)],
61}
62
63impl From<ReclaimBuffer<'_>> for Instruction {
64 fn from(builder: ReclaimBuffer<'_>) -> Self {
65 let mut accounts = vec![
66 AccountMeta::new_readonly(builder.state_pda, false),
67 AccountMeta::new_readonly(builder.reclaim_authority, true),
68 AccountMeta::new(builder.reclaim_recipient, false),
69 AccountMeta::new_readonly(builder.token_program, false),
70 ];
71 let mut data = vec![SettlementInstruction::ReclaimBuffer.discriminator()];
72 for (buffer_pda, mint, burn_limit) in builder.buffers {
73 accounts.push(AccountMeta::new(*buffer_pda, false));
74 accounts.push(AccountMeta::new(*mint, false));
77 data.extend_from_slice(&burn_limit.to_le_bytes());
78 }
79 Instruction {
80 program_id: builder.program_id,
81 accounts,
82 data,
83 }
84 }
85}
86
87pub struct Buffer<'a, A> {
89 pub buffer_pda: &'a A,
90 pub mint: &'a A,
91 pub burn_limit: u64,
92}
93
94pub struct Buffers<'a, A> {
98 accounts: &'a [[A; ACCOUNTS_PER_BUFFER]],
99 burn_limits: &'a [[u8; core::mem::size_of::<u64>()]],
100}
101
102impl<'a, A> Buffers<'a, A> {
103 pub fn iter(&self) -> impl Iterator<Item = Buffer<'a, A>> + '_ {
104 self.accounts
105 .iter()
106 .zip(self.burn_limits)
107 .map(|([buffer_pda, mint], burn_limit)| Buffer {
108 buffer_pda,
109 mint,
110 burn_limit: u64::from_le_bytes(*burn_limit),
111 })
112 }
113}
114
115pub struct ReclaimBufferInput<'a, A> {
117 pub state_pda: &'a A,
118 pub reclaim_authority: &'a A,
119 pub reclaim_recipient: &'a A,
120 pub buffers: Buffers<'a, A>,
121}
122
123impl<'a, A> InstructionInputParsing<'a, A> for ReclaimBufferInput<'a, A> {
124 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::ReclaimBuffer;
125
126 fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
127 let [state_pda, reclaim_authority, reclaim_recipient, _token_program, rest @ ..] = accounts
134 else {
135 return Err(ProgramError::NotEnoughAccountKeys);
136 };
137 let (buffer_pairs, remainder) = rest.as_chunks::<ACCOUNTS_PER_BUFFER>();
142 if !remainder.is_empty() || buffer_pairs.is_empty() {
143 return Err(ProgramError::NotEnoughAccountKeys);
144 }
145
146 let (burn_limits, remainder) =
150 instruction_data.as_chunks::<{ core::mem::size_of::<u64>() }>();
151 if !remainder.is_empty() || burn_limits.len() != buffer_pairs.len() {
152 return Err(ProgramError::InvalidInstructionData);
153 }
154
155 Ok(Self {
156 state_pda,
157 reclaim_authority,
158 reclaim_recipient,
159 buffers: Buffers {
160 accounts: buffer_pairs,
161 burn_limits,
162 },
163 })
164 }
165}
166
167#[cfg(any(test, feature = "test-fixtures"))]
170pub mod fixtures {
171 use solana_address::Address;
172
173 use super::{Instruction, ReclaimBuffer};
174
175 pub const NUM_SHARED_ACCOUNTS: usize = 4;
179
180 pub fn reclaim_buffer_data() -> Vec<u8> {
183 let zero = Address::new_from_array([0; 32]);
184 Instruction::from(ReclaimBuffer {
185 program_id: zero,
186 state_pda: zero,
187 reclaim_authority: zero,
188 reclaim_recipient: zero,
189 token_program: zero,
190 buffers: &[(zero, zero, 0)],
191 })
192 .data
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::fixtures::{reclaim_buffer_data, NUM_SHARED_ACCOUNTS};
199 use super::*;
200 use crate::fixtures::pubkey_from_seed;
201 use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
202 use crate::instruction::tests::{
203 assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
204 };
205
206 #[test]
207 fn reclaim_buffer_input_parses_valid_input() {
208 let program_id = pubkey_from_seed("program id");
209 let state_pda = pubkey_from_seed("state pda");
210 let reclaim_authority = pubkey_from_seed("reclaim authority");
211 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
212 let token_program = pubkey_from_seed("token program");
213 let buffer_pda = pubkey_from_seed("buffer pda");
214 let mint = pubkey_from_seed("mint");
215
216 let data = Instruction::from(ReclaimBuffer {
217 program_id,
218 state_pda,
219 reclaim_authority,
220 reclaim_recipient,
221 token_program,
222 buffers: &[(buffer_pda, mint, 1337)],
223 })
224 .data;
225 let token_program = pubkey_from_seed("token program");
226 let accounts = [
227 fake_account(state_pda),
228 fake_account(reclaim_authority),
229 fake_account(reclaim_recipient),
230 fake_account(token_program),
231 fake_account(buffer_pda),
232 fake_account(mint),
233 ];
234
235 let ReclaimBufferInput {
236 state_pda: parsed_state_pda,
237 reclaim_authority: parsed_reclaim_authority,
238 reclaim_recipient: parsed_reclaim_recipient,
239 buffers,
240 } = ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
241
242 assert_eq!(*parsed_state_pda.address(), state_pda);
243 assert_eq!(*parsed_reclaim_authority.address(), reclaim_authority);
244 assert_eq!(*parsed_reclaim_recipient.address(), reclaim_recipient);
245 let parsed: Vec<_> = buffers.iter().collect();
246 let [Buffer {
247 buffer_pda: parsed_buffer_pda,
248 mint: parsed_mint,
249 burn_limit,
250 }] = parsed.as_slice()
251 else {
252 panic!("expected exactly one parsed buffer");
253 };
254 assert_eq!(*parsed_buffer_pda.address(), buffer_pda);
255 assert_eq!(*parsed_mint.address(), mint);
256 assert_eq!(*burn_limit, 1337);
257 }
258
259 #[test]
260 fn reclaim_buffer_input_parses_multiple_buffers() {
261 let program_id = pubkey_from_seed("program id");
262 let state_pda = pubkey_from_seed("state pda");
263 let reclaim_authority = pubkey_from_seed("reclaim authority");
264 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
265 let token_program = pubkey_from_seed("token program");
266 let buffer_a = pubkey_from_seed("buffer a");
267 let mint_a = pubkey_from_seed("mint a");
268 let buffer_b = pubkey_from_seed("buffer b");
269 let mint_b = pubkey_from_seed("mint b");
270
271 let data = Instruction::from(ReclaimBuffer {
272 program_id,
273 state_pda,
274 token_program,
275 reclaim_authority,
276 reclaim_recipient,
277 buffers: &[(buffer_a, mint_a, 42), (buffer_b, mint_b, 1337)],
278 })
279 .data;
280 let accounts = [
281 fake_account(state_pda),
282 fake_account(reclaim_authority),
283 fake_account(reclaim_recipient),
284 fake_account(token_program),
285 fake_account(buffer_a),
286 fake_account(mint_a),
287 fake_account(buffer_b),
288 fake_account(mint_b),
289 ];
290
291 let ReclaimBufferInput { buffers, .. } =
292 ReclaimBufferInput::parse(&data, &accounts).expect("parse should succeed");
293
294 let parsed: Vec<_> = buffers.iter().collect();
295 let [Buffer {
296 buffer_pda: parsed_buffer_a,
297 mint: parsed_mint_a,
298 burn_limit: limit_a,
299 }, Buffer {
300 buffer_pda: parsed_buffer_b,
301 mint: parsed_mint_b,
302 burn_limit: limit_b,
303 }] = parsed.as_slice()
304 else {
305 panic!("expected exactly two parsed buffers");
306 };
307 assert_eq!(*parsed_buffer_a.address(), buffer_a);
308 assert_eq!(*parsed_mint_a.address(), mint_a);
309 assert_eq!(*limit_a, 42);
310 assert_eq!(*parsed_buffer_b.address(), buffer_b);
311 assert_eq!(*parsed_mint_b.address(), mint_b);
312 assert_eq!(*limit_b, 1337);
313 }
314
315 #[test]
316 fn reclaim_buffer_input_rejects_zero_buffers() {
317 let data = reclaim_buffer_data();
318 let accounts = fake_sequential_accounts::<NUM_SHARED_ACCOUNTS>();
320 assert_eq!(
321 ReclaimBufferInput::parse(&data, &accounts).err(),
322 Some(ProgramError::NotEnoughAccountKeys),
323 "an instruction that reclaims no buffers is rejected",
324 );
325 }
326
327 #[test]
328 fn reclaim_buffer_input_rejects_a_partial_trailing_limit() {
329 let mut data = reclaim_buffer_data();
330 data.push(0); let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + ACCOUNTS_PER_BUFFER }>();
332 assert_eq!(
333 ReclaimBufferInput::parse(&data, &accounts).err(),
334 Some(ProgramError::InvalidInstructionData),
335 );
336 }
337
338 #[test]
339 fn reclaim_buffer_input_rejects_a_limit_count_that_does_not_match_the_buffers() {
340 let data = reclaim_buffer_data();
342 let accounts =
343 fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + 2 * ACCOUNTS_PER_BUFFER }>();
344 assert_eq!(
345 ReclaimBufferInput::parse(&data, &accounts).err(),
346 Some(ProgramError::InvalidInstructionData),
347 );
348 }
349
350 #[test]
351 fn reclaim_buffer_input_rejects_missing_accounts() {
352 let data = reclaim_buffer_data();
353 let accounts = fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS - 1 }>();
355 assert_eq!(
356 ReclaimBufferInput::parse(&data, &accounts).err(),
357 Some(ProgramError::NotEnoughAccountKeys),
358 );
359 }
360
361 #[test]
362 fn reclaim_buffer_input_rejects_incomplete_pair() {
363 let data = reclaim_buffer_data();
364 let accounts =
367 fake_sequential_accounts::<{ NUM_SHARED_ACCOUNTS + ACCOUNTS_PER_BUFFER + 1 }>();
368 assert_eq!(
369 ReclaimBufferInput::parse(&data, &accounts).err(),
370 Some(ProgramError::NotEnoughAccountKeys),
371 );
372 }
373
374 #[test]
375 fn instruction_data_has_expected_layout() {
376 let program_id = pubkey_from_seed("program id");
377 let state_pda = pubkey_from_seed("state pda");
378 let reclaim_authority = pubkey_from_seed("reclaim authority");
379 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
380 let token_program = pubkey_from_seed("token program");
381 let buffer_pda = pubkey_from_seed("buffer pda");
382 let mint = pubkey_from_seed("mint");
383 let Instruction { data, .. } = ReclaimBuffer {
384 program_id,
385 state_pda,
386 reclaim_authority,
387 reclaim_recipient,
388 token_program,
389 buffers: &[(buffer_pda, mint, 0x1337)],
390 }
391 .into();
392 let mut expected = vec![SettlementInstruction::ReclaimBuffer.discriminator()];
393 expected.extend_from_slice(&[0x37, 0x13, 0, 0, 0, 0, 0, 0]);
394 assert_eq!(data, expected);
395 }
396
397 #[test]
398 fn single_buffer_has_expected_accounts() {
399 let program_id = pubkey_from_seed("program id");
400 let state_pda = pubkey_from_seed("state pda");
401 let reclaim_authority = pubkey_from_seed("reclaim authority");
402 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
403 let token_program = pubkey_from_seed("token program");
404 let buffer_pda = pubkey_from_seed("buffer pda");
405 let mint = pubkey_from_seed("mint");
406 let Instruction { accounts, .. } = ReclaimBuffer {
407 program_id,
408 state_pda,
409 reclaim_authority,
410 reclaim_recipient,
411 token_program,
412 buffers: &[(buffer_pda, mint, 0)],
413 }
414 .into();
415
416 assert_eq!(accounts.len(), 6);
417 assert_readonly_nonsigner(&accounts[0], state_pda);
418 assert_readonly_signer(&accounts[1], reclaim_authority);
419 assert_writable_nonsigner(&accounts[2], reclaim_recipient);
420 assert_readonly_nonsigner(&accounts[3], token_program);
421 assert_writable_nonsigner(&accounts[4], buffer_pda);
422 assert_writable_nonsigner(&accounts[5], mint);
423 }
424
425 #[test]
426 fn recipient_may_be_the_reclaim_authority_itself() {
427 let reclaim_authority = pubkey_from_seed("reclaim authority");
428 let Instruction { accounts, .. } = ReclaimBuffer {
429 program_id: pubkey_from_seed("program id"),
430 state_pda: pubkey_from_seed("state pda"),
431 reclaim_authority,
432 reclaim_recipient: reclaim_authority,
433 token_program: pubkey_from_seed("token program"),
434 buffers: &[(pubkey_from_seed("buffer pda"), pubkey_from_seed("mint"), 0)],
435 }
436 .into();
437
438 assert_readonly_signer(&accounts[1], reclaim_authority);
439 assert_writable_nonsigner(&accounts[2], reclaim_authority);
440 }
441
442 #[test]
443 fn multiple_buffers_append_pairs_after_shared_accounts() {
444 let program_id = pubkey_from_seed("program id");
445 let state_pda = pubkey_from_seed("state pda");
446 let reclaim_authority = pubkey_from_seed("reclaim authority");
447 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
448 let token_program = pubkey_from_seed("token program");
449 let buffer_a = pubkey_from_seed("buffer a");
450 let mint_a = pubkey_from_seed("mint a");
451 let buffer_b = pubkey_from_seed("buffer b");
452 let mint_b = pubkey_from_seed("mint b");
453 let Instruction { accounts, .. } = ReclaimBuffer {
454 program_id,
455 state_pda,
456 reclaim_authority,
457 reclaim_recipient,
458 token_program,
459 buffers: &[(buffer_a, mint_a, 0), (buffer_b, mint_b, 0)],
460 }
461 .into();
462
463 assert_eq!(
465 accounts.len(),
466 NUM_SHARED_ACCOUNTS + 2 * ACCOUNTS_PER_BUFFER
467 );
468 assert_writable_nonsigner(&accounts[4], buffer_a);
469 assert_writable_nonsigner(&accounts[5], mint_a);
470 assert_writable_nonsigner(&accounts[6], buffer_b);
471 assert_writable_nonsigner(&accounts[7], mint_b);
472 }
473
474 #[test]
475 fn empty_buffers_has_only_shared_accounts() {
476 let program_id = pubkey_from_seed("program id");
477 let state_pda = pubkey_from_seed("state pda");
478 let reclaim_authority = pubkey_from_seed("reclaim authority");
479 let reclaim_recipient = pubkey_from_seed("reclaim recipient");
480 let token_program = pubkey_from_seed("token program");
481 let Instruction { accounts, .. } = ReclaimBuffer {
482 program_id,
483 state_pda,
484 reclaim_authority,
485 reclaim_recipient,
486 token_program,
487 buffers: &[],
488 }
489 .into();
490 assert_eq!(accounts.len(), NUM_SHARED_ACCOUNTS);
491 }
492}