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