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