1use std::vec;
4
5use solana_instruction::{AccountMeta, Instruction};
6use solana_program_error::ProgramError;
7use solana_pubkey::Pubkey;
8
9use crate::instruction::InstructionInputParsing;
10use crate::{recover_discriminator, SettlementError, SettlementInstruction};
11
12use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID};
13
14pub const FINALIZE_FIXED_ACCOUNTS: usize = 3;
18
19fn split_push_bytes(body: &[u8]) -> Result<(&[u8], &[[u8; 8]]), ProgramError> {
23 if !body.len().is_multiple_of(9) {
28 return Err(ProgramError::InvalidInstructionData);
29 }
30 let push_count = body.len() / 9;
31 let (bumps, amount_bytes) = body.split_at(push_count);
32 let (amounts, []) = amount_bytes.as_chunks::<8>() else {
33 return Err(ProgramError::InvalidInstructionData);
34 };
35 Ok((bumps, amounts))
36}
37
38fn split_pushes(body: &[u8]) -> Result<(&[u8], impl Iterator<Item = u64> + '_), ProgramError> {
42 let (bumps, amounts) = split_push_bytes(body)?;
43 Ok((bumps, amounts.iter().copied().map(u64::from_le_bytes)))
44}
45
46pub fn finalize_push_data(
55 instruction_data: &[u8],
56) -> Result<impl Iterator<Item = (u8, u64)> + '_, ProgramError> {
57 let (_discriminator, rest) = recover_discriminator(instruction_data)?;
58 let (_begin_ix_index, body) = recover_counterpart(rest)?;
59 let (bumps, amounts) = split_pushes(body)?;
60 Ok(bumps.iter().copied().zip(amounts))
61}
62
63pub struct FinalizeSettle<'a> {
89 pub program_id: Pubkey,
90 pub state_pda: Pubkey,
91 pub begin_ix_index: u16,
92 pub source_buffers: &'a [Pubkey],
93 pub destinations: &'a [Pubkey],
94 pub bumps: &'a [u8],
95 pub amounts: &'a [u64],
96}
97
98impl From<FinalizeSettle<'_>> for Instruction {
99 fn from(builder: FinalizeSettle<'_>) -> Self {
100 let FinalizeSettle {
101 program_id,
102 state_pda,
103 begin_ix_index,
104 source_buffers,
105 destinations,
106 bumps,
107 amounts,
108 } = builder;
109
110 let data: Vec<u8> = core::iter::once(SettlementInstruction::FinalizeSettle.discriminator())
111 .chain(begin_ix_index.to_le_bytes())
112 .chain(bumps.iter().copied())
113 .chain(amounts.iter().flat_map(|amount| amount.to_le_bytes()))
114 .collect();
115
116 let mut accounts = vec![
117 AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
118 AccountMeta::new_readonly(state_pda, false),
119 AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
120 ];
121 for (source, destination) in source_buffers.iter().zip(destinations) {
122 accounts.push(AccountMeta::new(*source, false));
123 accounts.push(AccountMeta::new(*destination, false));
124 }
125
126 Instruction {
127 program_id,
128 accounts,
129 data,
130 }
131 }
132}
133
134#[derive(Debug, PartialEq, Eq)]
138pub struct Push<'a, A> {
139 pub source_buffer: &'a A,
140 pub destination: &'a A,
141 pub bump: u8,
142 pub amount: u64,
143}
144
145pub struct Pushes<'a, A> {
151 push_accounts: &'a [A],
153 bumps: &'a [u8],
154 amounts: &'a [[u8; 8]],
156}
157
158impl<'a, A> Pushes<'a, A> {
159 #[allow(
161 clippy::arithmetic_side_effects,
162 reason = "offsets are bounded by tx limits"
163 )]
164 pub fn iter(&self) -> impl Iterator<Item = Push<'a, A>> + '_ {
165 let push_count = self.bumps.len();
166 let mut i = 0usize;
167 let mut account_offset = 0usize;
168 std::iter::from_fn(move || {
169 if i >= push_count {
170 return None;
171 }
172 let bump = self.bumps[i];
173 let amount = u64::from_le_bytes(self.amounts[i]);
174 i += 1;
175
176 let source_buffer = &self.push_accounts[account_offset];
177 let destination = &self.push_accounts[account_offset + 1];
178 account_offset += 2;
179
180 Some(Push {
181 source_buffer,
182 destination,
183 bump,
184 amount,
185 })
186 })
187 }
188}
189
190pub struct FinalizeSettleInput<'a, A> {
198 pub begin_ix_index: u16,
199 pub instructions_sysvar_account: &'a A,
200 pub state_pda_account: &'a A,
201 pub token_program_account: &'a A,
202 pub pushes: Pushes<'a, A>,
203}
204
205impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> {
209 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::FinalizeSettle;
210
211 fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
212 let (begin_ix_index, body) = recover_counterpart(instruction_data)?;
213
214 let [instructions_sysvar_account, state_pda_account, token_program_account, push_accounts @ ..] =
215 accounts
216 else {
217 return Err(ProgramError::NotEnoughAccountKeys);
218 };
219
220 let (bumps, amounts) = split_push_bytes(body)?;
221 let push_count = bumps.len();
222
223 let expected_accounts = push_count
226 .checked_mul(2)
227 .ok_or(ProgramError::InvalidInstructionData)?;
228 if push_accounts.len() != expected_accounts {
229 return Err(SettlementError::AccountCountNotMatchingPushCount.into());
230 }
231
232 Ok(Self {
233 begin_ix_index,
234 instructions_sysvar_account,
235 state_pda_account,
236 token_program_account,
237 pushes: Pushes {
238 push_accounts,
239 bumps,
240 amounts,
241 },
242 })
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::fixtures::pubkey_from_seed;
250 use crate::instruction::fixtures::{
251 fake_account, fake_account_from_array, fake_sequential_accounts,
252 };
253 use crate::instruction::settle::tests::ix_data;
254 use crate::instruction::tests::assert_readonly_nonsigner;
255 use hex_literal::hex;
256 use proptest::prelude::*;
257 use solana_account_view::AccountView;
258 use solana_address::Address;
259
260 #[test]
261 fn finalize_fixed_accounts_matches_builder() {
262 let ix = Instruction::from(FinalizeSettle {
265 program_id: pubkey_from_seed("program id"),
266 state_pda: pubkey_from_seed("state pda"),
267 begin_ix_index: 0,
268 source_buffers: &[],
269 destinations: &[],
270 bumps: &[],
271 amounts: &[],
272 });
273 assert_eq!(ix.accounts.len(), FINALIZE_FIXED_ACCOUNTS);
274 }
275
276 #[test]
277 fn expected_encoding_finalize_settle_no_pushes() {
278 let program_id = pubkey_from_seed("program id");
279 let state_pda = pubkey_from_seed("state pda");
280 let Instruction {
281 program_id: ix_program_id,
282 accounts,
283 data,
284 } = FinalizeSettle {
285 program_id,
286 state_pda,
287 begin_ix_index: 0x1337,
288 source_buffers: &[],
289 destinations: &[],
290 bumps: &[],
291 amounts: &[],
292 }
293 .into();
294 assert_eq!(ix_program_id, program_id);
295 assert_eq!(
296 data,
297 ix_data![
298 [SettlementInstruction::FinalizeSettle.discriminator()],
299 hex!("3713"), ],
301 );
302 assert_eq!(accounts.len(), 3);
307 assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID);
308 assert_readonly_nonsigner(&accounts[1], state_pda);
309 assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID);
310 }
311
312 #[test]
313 fn finalize_settle_encodes_pushes() {
314 let program_id = pubkey_from_seed("program id");
315 let state_pda = pubkey_from_seed("state pda");
316 let source_a = Pubkey::new_from_array([0x01; 32]);
317 let dest_a = Pubkey::new_from_array([0x02; 32]);
318 let source_b = Pubkey::new_from_array([0x03; 32]);
319 let dest_b = Pubkey::new_from_array([0x04; 32]);
320
321 let ix = Instruction::from(FinalizeSettle {
322 program_id,
323 state_pda,
324 begin_ix_index: 0x1337,
325 source_buffers: &[source_a, source_b],
326 destinations: &[dest_a, dest_b],
327 bumps: &[0xa1, 0xb1],
328 amounts: &[0x01020304, 0x05060708],
329 });
330
331 assert_eq!(
332 ix.data,
333 ix_data![
334 [SettlementInstruction::FinalizeSettle.discriminator()],
335 hex!("3713"), [0xa1, 0xb1], hex!("0403020100000000"),
339 hex!("0807060500000000"),
340 ],
341 );
342
343 let actual: Vec<Pubkey> = ix.accounts.iter().map(|meta| meta.pubkey).collect();
344 assert_eq!(
345 actual,
346 vec![
347 INSTRUCTIONS_SYSVAR_ID,
348 state_pda,
349 SPL_TOKEN_PROGRAM_ID,
350 source_a,
351 dest_a,
352 source_b,
353 dest_b,
354 ],
355 );
356 let writable: Vec<Pubkey> = ix
359 .accounts
360 .iter()
361 .filter(|meta| meta.is_writable)
362 .map(|meta| meta.pubkey)
363 .collect();
364 assert_eq!(writable, vec![source_a, dest_a, source_b, dest_b]);
365 assert!(ix.accounts.iter().all(|meta| !meta.is_signer));
366 }
367
368 #[test]
369 fn finalize_settle_input_parses_no_pushes() {
370 let sysvar = pubkey_from_seed("sysvar");
371 let state = pubkey_from_seed("state pda");
372 let token_program = pubkey_from_seed("token program");
373 let accounts = [
374 fake_account(sysvar),
375 fake_account(state),
376 fake_account(token_program),
377 ];
378 let data = ix_data![
379 [SettlementInstruction::FinalizeSettle.discriminator()],
380 [0x37, 0x13], ];
382 let FinalizeSettleInput {
383 begin_ix_index,
384 instructions_sysvar_account,
385 state_pda_account,
386 token_program_account,
387 pushes,
388 } = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
389 assert_eq!(begin_ix_index, 0x1337);
390 assert_eq!(instructions_sysvar_account.address(), &sysvar);
391 assert_eq!(state_pda_account.address(), &state);
392 assert_eq!(token_program_account.address(), &token_program);
393 assert_eq!(pushes.iter().count(), 0);
394 }
395
396 #[test]
397 fn finalize_settle_input_parses_pushes() {
398 let sysvar = pubkey_from_seed("sysvar");
399 let state = pubkey_from_seed("state pda");
400 let token_program = pubkey_from_seed("token program");
401 let source = pubkey_from_seed("source buffer");
404 let dest0 = pubkey_from_seed("destination 0");
405 let dest1 = pubkey_from_seed("destination 1");
406 let accounts = [
407 fake_account(sysvar),
408 fake_account(state),
409 fake_account(token_program),
410 fake_account(source),
411 fake_account(dest0),
412 fake_account(source),
413 fake_account(dest1),
414 ];
415 let data = ix_data![
416 [SettlementInstruction::FinalizeSettle.discriminator()],
417 [0x13, 0x37], [0xfe, 0xfd], 0x1122u64.to_le_bytes(),
420 0x3344u64.to_le_bytes(),
421 ];
422
423 let FinalizeSettleInput { pushes, .. } =
424 FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
425
426 let parsed: Vec<(&Address, &Address, u8, u64)> = pushes
427 .iter()
428 .map(|push| {
429 (
430 push.source_buffer.address(),
431 push.destination.address(),
432 push.bump,
433 push.amount,
434 )
435 })
436 .collect();
437 assert_eq!(
438 parsed,
439 vec![
440 (&source, &dest0, 0xfe, 0x1122),
441 (&source, &dest1, 0xfd, 0x3344),
442 ],
443 );
444 }
445
446 #[test]
447 fn finalize_settle_input_parses_many_pushes() {
448 const PUSH_COUNT: usize = 16;
449
450 struct ExpectedPush {
451 source: Address,
452 dest: Address,
453 bump: u8,
454 amount: u64,
455 }
456 let mut expected: Vec<ExpectedPush> = Vec::new();
457 for i in 0..PUSH_COUNT {
458 let source = pubkey_from_seed(&format!("source buffer {i}"));
459 let dest = pubkey_from_seed(&format!("destination {i}"));
460 let bump = (i + 2 * PUSH_COUNT) as u8;
461 let amount = u64::from_le_bytes([(i + 3 * PUSH_COUNT) as u8; 8]);
462 expected.push(ExpectedPush {
463 source,
464 dest,
465 bump,
466 amount,
467 });
468 }
469
470 let mut accounts = vec![
473 fake_account_from_array([0xff; 32]),
474 fake_account_from_array([0xfe; 32]),
475 fake_account_from_array([0xfd; 32]),
476 ];
477 let mut bump_bytes = Vec::new();
478 let mut amount_bytes = Vec::new();
479 for push in &expected {
480 accounts.push(fake_account(push.source));
481 accounts.push(fake_account(push.dest));
482 bump_bytes.push(push.bump);
483 amount_bytes.extend_from_slice(&push.amount.to_le_bytes());
484 }
485 let data = ix_data![
486 [SettlementInstruction::FinalizeSettle.discriminator()],
487 [0x13, 0x37], bump_bytes,
489 amount_bytes,
490 ];
491
492 let parsed = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
493 let pushes: Vec<_> = parsed.pushes.iter().collect();
494
495 assert_eq!(pushes.len(), PUSH_COUNT);
496 for (push, expected) in pushes.iter().zip(&expected) {
497 assert_eq!(push.source_buffer.address(), &expected.source);
498 assert_eq!(push.destination.address(), &expected.dest);
499 assert_eq!(push.bump, expected.bump);
500 assert_eq!(push.amount, expected.amount);
501 }
502 }
503
504 #[test]
505 fn finalize_settle_input_rejects_different_discriminator() {
506 let data = ix_data![
507 [SettlementInstruction::BeginSettle.discriminator()],
508 [0, 0], ];
510 let accounts: [AccountView; 0] = [];
511 assert_eq!(
512 FinalizeSettleInput::parse(&data, &accounts).err(),
513 Some(ProgramError::InvalidInstructionData),
514 );
515 }
516
517 #[test]
518 fn finalize_settle_input_rejects_empty_accounts() {
519 let data = ix_data![
520 [SettlementInstruction::FinalizeSettle.discriminator()],
521 [0, 0], ];
523 let accounts: [AccountView; 0] = [];
524 assert_eq!(
525 FinalizeSettleInput::parse(&data, &accounts).err(),
526 Some(ProgramError::NotEnoughAccountKeys),
527 );
528 }
529
530 #[test]
531 fn finalize_settle_input_rejects_account_count_mismatch() {
532 let data: Vec<u8> = ix_data![
535 [SettlementInstruction::FinalizeSettle.discriminator()],
536 [13, 37], [0xff], 31337u64.to_le_bytes(),
539 ];
540
541 let too_few = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 1 }>();
543 assert_eq!(
544 FinalizeSettleInput::parse(&data, &too_few).err(),
545 Some(SettlementError::AccountCountNotMatchingPushCount.into()),
546 );
547
548 let too_many = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 3 }>();
550 assert_eq!(
551 FinalizeSettleInput::parse(&data, &too_many).err(),
552 Some(SettlementError::AccountCountNotMatchingPushCount.into()),
553 );
554 }
555
556 #[test]
557 fn finalize_settle_input_rejects_partial_push() {
558 let accounts = fake_sequential_accounts::<FINALIZE_FIXED_ACCOUNTS>();
561 let data = ix_data![
562 [SettlementInstruction::FinalizeSettle.discriminator()],
563 [37, 13], [0xff], [0x11, 0x22, 0x33, 0x44], ];
567 assert_eq!(
568 FinalizeSettleInput::parse(&data, &accounts).err(),
569 Some(ProgramError::InvalidInstructionData),
570 );
571 }
572
573 #[test]
574 fn finalize_push_data_extracts_bumps_and_amounts() {
575 let bumps = [0xa1, 0xb1];
576 let amounts = [0x0102, 0x0304];
577 let ix = Instruction::from(FinalizeSettle {
578 program_id: pubkey_from_seed("program id"),
579 state_pda: pubkey_from_seed("state pda"),
580 begin_ix_index: 0x1337,
581 source_buffers: &[
582 pubkey_from_seed("source buffer 0"),
583 pubkey_from_seed("source buffer 1"),
584 ],
585 destinations: &[
586 pubkey_from_seed("destination 0"),
587 pubkey_from_seed("destination 1"),
588 ],
589 bumps: &bumps,
590 amounts: &amounts,
591 });
592
593 let recovered = finalize_push_data(&ix.data).expect("valid finalize data");
594 let decoded: Vec<(u8, u64)> = recovered.collect();
595 assert_eq!(decoded, [(0xa1, 0x0102), (0xb1, 0x0304)]);
596 }
597
598 #[test]
599 fn finalize_push_data_handles_no_pushes() {
600 let ix = Instruction::from(FinalizeSettle {
601 program_id: pubkey_from_seed("program id"),
602 state_pda: pubkey_from_seed("state pda"),
603 begin_ix_index: 0,
604 source_buffers: &[],
605 destinations: &[],
606 bumps: &[],
607 amounts: &[],
608 });
609 let mut pushes = finalize_push_data(&ix.data).expect("valid empty finalize data");
610 assert!(pushes.next().is_none());
611 }
612
613 #[test]
614 fn finalize_push_data_rejects_incorrect_bytes() {
615 let mut ix = Instruction::from(FinalizeSettle {
616 program_id: pubkey_from_seed("program id"),
617 state_pda: pubkey_from_seed("state pda"),
618 begin_ix_index: 0,
619 source_buffers: &[pubkey_from_seed("source buffer")],
620 destinations: &[pubkey_from_seed("destination")],
621 bumps: &[0xff],
622 amounts: &[31337],
623 });
624 ix.data.pop();
625 assert_eq!(
626 finalize_push_data(&ix.data).err(),
627 Some(ProgramError::InvalidInstructionData),
628 );
629 }
630
631 fn arb_finalize_instruction(
634 push_count: impl Into<prop::collection::SizeRange>,
635 ) -> impl Strategy<Value = Instruction> {
636 (
637 any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
638 any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
639 any::<u16>(),
640 crate::instruction::settle::fixtures::arb_pushes(push_count),
641 )
642 .prop_map(
643 |(
644 program_id,
645 state_pda,
646 begin_ix_index,
647 (source_buffers, destinations, bumps, amounts),
648 )| {
649 Instruction::from(FinalizeSettle {
650 program_id,
651 state_pda,
652 begin_ix_index,
653 source_buffers: &source_buffers,
654 destinations: &destinations,
655 bumps: &bumps,
656 amounts: &amounts,
657 })
658 },
659 )
660 }
661
662 proptest! {
663 #[test]
668 fn finalize_push_data_matches_parser(ix in arb_finalize_instruction(0..=16usize)) {
669 let recovered: Vec<(u8, u64)> =
671 finalize_push_data(&ix.data).expect("well-formed finalize data").collect();
672
673 let accounts: Vec<AccountView> =
675 ix.accounts.iter().map(|meta| fake_account(meta.pubkey)).collect();
676 let parsed = FinalizeSettleInput::parse(&ix.data, &accounts)
677 .expect("a well-formed finalize parses");
678 let parsed_pushes: Vec<(u8, u64)> =
679 parsed.pushes.iter().map(|push| (push.bump, push.amount)).collect();
680
681 prop_assert_eq!(recovered, parsed_pushes);
682 }
683 }
684}