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::{
13 recover_counterpart, MaybeMint, TokenProgram, INSTRUCTIONS_SYSVAR_ID, MINT_PLACEHOLDER,
14};
15
16pub const FINALIZE_FIXED_ACCOUNTS: usize = 4;
20
21pub const FINALIZE_PUSH_ACCOUNTS: usize = 3;
24
25fn split_push_bytes(body: &[u8]) -> Result<(&[u8], &[[u8; 8]]), ProgramError> {
29 if !body.len().is_multiple_of(9) {
34 return Err(ProgramError::InvalidInstructionData);
35 }
36 let push_count = body.len() / 9;
37 let (bumps, amount_bytes) = body.split_at(push_count);
38 let (amounts, []) = amount_bytes.as_chunks::<8>() else {
39 return Err(ProgramError::InvalidInstructionData);
40 };
41 Ok((bumps, amounts))
42}
43
44pub fn finalize_push_data(instruction_data: &[u8]) -> Result<(&[u8], &[[u8; 8]]), ProgramError> {
54 let (_discriminator, rest) = recover_discriminator(instruction_data)?;
55 let (_begin_ix_index, body) = recover_counterpart(rest)?;
56 split_push_bytes(body)
57}
58
59#[cfg_attr(any(test, feature = "test-fixtures"), derive(Default))]
97pub struct FinalizeSettle<'a> {
98 pub program_id: Pubkey,
99 pub state_pda: Pubkey,
100 pub begin_ix_index: u16,
101 pub only_token_program: Option<TokenProgram>,
104 pub source_buffers: &'a [Pubkey],
105 pub destinations: &'a [Pubkey],
106 pub mints: &'a [Option<Pubkey>],
107 pub bumps: &'a [u8],
108 pub amounts: &'a [u64],
109}
110
111impl From<FinalizeSettle<'_>> for Instruction {
112 fn from(builder: FinalizeSettle<'_>) -> Self {
113 let FinalizeSettle {
114 program_id,
115 state_pda,
116 begin_ix_index,
117 only_token_program,
118 source_buffers,
119 destinations,
120 mints,
121 bumps,
122 amounts,
123 } = builder;
124
125 let data: Vec<u8> = core::iter::once(SettlementInstruction::FinalizeSettle.discriminator())
126 .chain(begin_ix_index.to_le_bytes())
127 .chain(bumps.iter().copied())
128 .chain(amounts.iter().flat_map(|amount| amount.to_le_bytes()))
129 .collect();
130
131 let mut accounts = vec![
132 AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
133 AccountMeta::new_readonly(state_pda, false),
134 ];
135 accounts.extend(TokenProgram::ALL.map(|program| {
139 let address = if only_token_program.is_none_or(|only| only == program) {
140 program.address()
141 } else {
142 INSTRUCTIONS_SYSVAR_ID
143 };
144 AccountMeta::new_readonly(address, false)
145 }));
146 for ((source, destination), mint) in source_buffers.iter().zip(destinations).zip(mints) {
147 accounts.push(AccountMeta::new(*source, false));
148 accounts.push(AccountMeta::new(*destination, false));
149 accounts.push(AccountMeta::new_readonly(
150 mint.unwrap_or(MINT_PLACEHOLDER),
151 false,
152 ));
153 }
154
155 Instruction {
156 program_id,
157 accounts,
158 data,
159 }
160 }
161}
162
163#[derive(Debug, PartialEq, Eq)]
168pub struct Push<'a, A> {
169 pub source_buffer: &'a A,
170 pub destination: &'a A,
171 pub mint: MaybeMint<'a, A>,
172 pub bump: u8,
173 pub amount: u64,
174}
175
176pub struct Pushes<'a, A> {
181 push_accounts: &'a [[A; FINALIZE_PUSH_ACCOUNTS]],
182 bumps: &'a [u8],
183 amounts: &'a [[u8; 8]],
185}
186
187impl<'a, A> Pushes<'a, A> {
188 pub fn iter(&self) -> impl Iterator<Item = Push<'a, A>> + '_ {
190 self.push_accounts
191 .iter()
192 .zip(self.bumps)
193 .zip(self.amounts)
194 .map(
195 |(([source_buffer, destination, mint], &bump), amount)| Push {
196 source_buffer,
197 destination,
198 mint: MaybeMint::new(mint),
199 bump,
200 amount: u64::from_le_bytes(*amount),
201 },
202 )
203 }
204}
205
206pub struct FinalizeSettleInput<'a, A> {
214 pub begin_ix_index: u16,
215 pub instructions_sysvar_account: &'a A,
216 pub state_pda_account: &'a A,
217 pub pushes: Pushes<'a, A>,
218}
219
220impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> {
224 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::FinalizeSettle;
225
226 fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
227 let (begin_ix_index, body) = recover_counterpart(instruction_data)?;
228
229 let [instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, push_accounts @ ..] =
232 accounts
233 else {
234 return Err(ProgramError::NotEnoughAccountKeys);
235 };
236
237 let (bumps, amounts) = split_push_bytes(body)?;
238 let push_count = bumps.len();
239
240 let (push_triples, []) = push_accounts.as_chunks::<FINALIZE_PUSH_ACCOUNTS>() else {
243 return Err(SettlementError::AccountCountNotMatchingPushCount.into());
244 };
245 if push_triples.len() != push_count {
246 return Err(SettlementError::AccountCountNotMatchingPushCount.into());
247 }
248
249 Ok(Self {
250 begin_ix_index,
251 instructions_sysvar_account,
252 state_pda_account,
253 pushes: Pushes {
254 push_accounts: push_triples,
255 bumps,
256 amounts,
257 },
258 })
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::fixtures::pubkey_from_seed;
266 use crate::instruction::fixtures::{
267 fake_account, fake_account_from_array, fake_sequential_accounts,
268 };
269 use crate::instruction::settle::tests::{ix_data, mint_address};
270 use crate::instruction::tests::assert_readonly_nonsigner;
271 use crate::token_program::TokenProgram;
272 use hex_literal::hex;
273 use proptest::prelude::*;
274 use solana_account_view::AccountView;
275 use solana_address::Address;
276
277 #[test]
278 fn finalize_fixed_accounts_matches_builder() {
279 let ix = Instruction::from(FinalizeSettle {
282 program_id: pubkey_from_seed("program id"),
283 state_pda: pubkey_from_seed("state pda"),
284 ..Default::default()
285 });
286 assert_eq!(ix.accounts.len(), FINALIZE_FIXED_ACCOUNTS);
287 }
288
289 #[test]
290 fn expected_encoding_finalize_settle_no_pushes() {
291 let program_id = pubkey_from_seed("program id");
292 let state_pda = pubkey_from_seed("state pda");
293 let Instruction {
294 program_id: ix_program_id,
295 accounts,
296 data,
297 } = FinalizeSettle {
298 program_id,
299 state_pda,
300 begin_ix_index: 0x1337,
301 ..Default::default()
302 }
303 .into();
304 assert_eq!(ix_program_id, program_id);
305 assert_eq!(
306 data,
307 ix_data![
308 [SettlementInstruction::FinalizeSettle.discriminator()],
309 hex!("3713"), ],
311 );
312 assert_eq!(accounts.len(), FINALIZE_FIXED_ACCOUNTS);
318 assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID);
319 assert_readonly_nonsigner(&accounts[1], state_pda);
320 assert_readonly_nonsigner(&accounts[2], TokenProgram::SplToken.address());
321 assert_readonly_nonsigner(&accounts[3], TokenProgram::Token2022.address());
322 }
323
324 #[test]
328 fn finalize_settle_carries_the_token_program_slots_it_is_given() {
329 for (only_token_program, expected) in [
330 (
331 None,
332 [
333 TokenProgram::SplToken.address(),
334 TokenProgram::Token2022.address(),
335 ],
336 ),
337 (
338 Some(TokenProgram::SplToken),
339 [TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID],
340 ),
341 (
342 Some(TokenProgram::Token2022),
343 [INSTRUCTIONS_SYSVAR_ID, TokenProgram::Token2022.address()],
344 ),
345 ] {
346 let ix = Instruction::from(FinalizeSettle {
347 program_id: Pubkey::new_unique(),
348 state_pda: Pubkey::new_unique(),
349 only_token_program,
350 ..Default::default()
351 });
352 let slots: Vec<Pubkey> = ix.accounts[2..].iter().map(|meta| meta.pubkey).collect();
353 assert_eq!(
354 slots, expected,
355 "{only_token_program:?} should name just the programs it settles against",
356 );
357 }
358 }
359
360 #[test]
361 fn finalize_settle_encodes_pushes() {
362 let program_id = pubkey_from_seed("program id");
363 let state_pda = pubkey_from_seed("state pda");
364 let source_a = Pubkey::new_from_array([0x01; 32]);
365 let dest_a = Pubkey::new_from_array([0x02; 32]);
366 let source_b = Pubkey::new_from_array([0x03; 32]);
367 let dest_b = Pubkey::new_from_array([0x04; 32]);
368 let mint_a = Pubkey::new_from_array([0x05; 32]);
369
370 let ix = Instruction::from(FinalizeSettle {
371 program_id,
372 state_pda,
373 begin_ix_index: 0x1337,
374 source_buffers: &[source_a, source_b],
375 destinations: &[dest_a, dest_b],
376 mints: &[Some(mint_a), None],
377 bumps: &[0xa1, 0xb1],
378 amounts: &[0x01020304, 0x05060708],
379 ..Default::default()
380 });
381
382 assert_eq!(
383 ix.data,
384 ix_data![
385 [SettlementInstruction::FinalizeSettle.discriminator()],
386 hex!("3713"), [0xa1, 0xb1], hex!("0403020100000000"),
390 hex!("0807060500000000"),
391 ],
392 );
393
394 let actual: Vec<Pubkey> = ix.accounts.iter().map(|meta| meta.pubkey).collect();
395 assert_eq!(
396 actual,
397 vec![
398 INSTRUCTIONS_SYSVAR_ID,
399 state_pda,
400 TokenProgram::SplToken.address(),
401 TokenProgram::Token2022.address(),
402 source_a,
403 dest_a,
404 mint_a,
405 source_b,
406 dest_b,
407 MINT_PLACEHOLDER,
408 ],
409 );
410 let writable: Vec<Pubkey> = ix
413 .accounts
414 .iter()
415 .filter(|meta| meta.is_writable)
416 .map(|meta| meta.pubkey)
417 .collect();
418 assert_eq!(writable, vec![source_a, dest_a, source_b, dest_b]);
419 assert!(ix.accounts.iter().all(|meta| !meta.is_signer));
420 }
421
422 #[test]
423 fn finalize_settle_input_parses_no_pushes() {
424 let sysvar = pubkey_from_seed("sysvar");
425 let state = pubkey_from_seed("state pda");
426 let spl_token_program = pubkey_from_seed("spl token program");
427 let token_2022_program = pubkey_from_seed("token 2022 program");
428 let accounts = [
429 fake_account(sysvar),
430 fake_account(state),
431 fake_account(spl_token_program),
432 fake_account(token_2022_program),
433 ];
434 let data = ix_data![
435 [SettlementInstruction::FinalizeSettle.discriminator()],
436 [0x37, 0x13], ];
438 let FinalizeSettleInput {
439 begin_ix_index,
440 instructions_sysvar_account,
441 state_pda_account,
442 pushes,
443 } = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
444 assert_eq!(begin_ix_index, 0x1337);
445 assert_eq!(instructions_sysvar_account.address(), &sysvar);
446 assert_eq!(state_pda_account.address(), &state);
447 assert_eq!(pushes.iter().count(), 0);
448 }
449
450 #[test]
451 fn finalize_settle_input_parses_pushes() {
452 let sysvar = pubkey_from_seed("sysvar");
453 let state = pubkey_from_seed("state pda");
454 let spl_token_program = pubkey_from_seed("spl token program");
455 let token_2022_program = pubkey_from_seed("token 2022 program");
456 let source = pubkey_from_seed("source buffer");
459 let dest0 = pubkey_from_seed("destination 0");
460 let dest1 = pubkey_from_seed("destination 1");
461 let mint0 = pubkey_from_seed("mint 0");
462 let mint1 = pubkey_from_seed("mint 1");
463 let accounts = [
464 fake_account(sysvar),
465 fake_account(state),
466 fake_account(spl_token_program),
467 fake_account(token_2022_program),
468 fake_account(source),
469 fake_account(dest0),
470 fake_account(mint0),
471 fake_account(source),
472 fake_account(dest1),
473 fake_account(mint1),
474 ];
475 let data = ix_data![
476 [SettlementInstruction::FinalizeSettle.discriminator()],
477 [0x13, 0x37], [0xfe, 0xfd], 0x1122u64.to_le_bytes(),
480 0x3344u64.to_le_bytes(),
481 ];
482
483 let FinalizeSettleInput { pushes, .. } =
484 FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
485
486 let parsed: Vec<(&Address, &Address, &Address, u8, u64)> = pushes
487 .iter()
488 .map(|push| {
489 (
490 push.source_buffer.address(),
491 push.destination.address(),
492 mint_address(push.mint),
493 push.bump,
494 push.amount,
495 )
496 })
497 .collect();
498 assert_eq!(
499 parsed,
500 vec![
501 (&source, &dest0, &mint0, 0xfe, 0x1122),
502 (&source, &dest1, &mint1, 0xfd, 0x3344),
503 ],
504 );
505 }
506
507 #[test]
508 fn finalize_settle_input_parses_many_pushes() {
509 const PUSH_COUNT: usize = 16;
510
511 struct ExpectedPush {
512 source: Address,
513 dest: Address,
514 mint: Address,
515 bump: u8,
516 amount: u64,
517 }
518 let mut expected: Vec<ExpectedPush> = Vec::new();
519 for i in 0..PUSH_COUNT {
520 let source = pubkey_from_seed(&format!("source buffer {i}"));
521 let dest = pubkey_from_seed(&format!("destination {i}"));
522 let mint = pubkey_from_seed(&format!("mint {i}"));
523 let bump = (i + 2 * PUSH_COUNT) as u8;
524 let amount = u64::from_le_bytes([(i + 3 * PUSH_COUNT) as u8; 8]);
525 expected.push(ExpectedPush {
526 source,
527 dest,
528 mint,
529 bump,
530 amount,
531 });
532 }
533
534 let mut accounts = vec![
537 fake_account_from_array([0xff; 32]),
538 fake_account_from_array([0xfe; 32]),
539 fake_account_from_array([0xfd; 32]),
540 fake_account_from_array([0xfc; 32]),
541 ];
542 let mut bump_bytes = Vec::new();
543 let mut amount_bytes = Vec::new();
544 for push in &expected {
545 accounts.push(fake_account(push.source));
546 accounts.push(fake_account(push.dest));
547 accounts.push(fake_account(push.mint));
548 bump_bytes.push(push.bump);
549 amount_bytes.extend_from_slice(&push.amount.to_le_bytes());
550 }
551 let data = ix_data![
552 [SettlementInstruction::FinalizeSettle.discriminator()],
553 [0x13, 0x37], bump_bytes,
555 amount_bytes,
556 ];
557
558 let parsed = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
559 let pushes: Vec<_> = parsed.pushes.iter().collect();
560
561 assert_eq!(pushes.len(), PUSH_COUNT);
562 for (push, expected) in pushes.iter().zip(&expected) {
563 assert_eq!(push.source_buffer.address(), &expected.source);
564 assert_eq!(push.destination.address(), &expected.dest);
565 assert_eq!(mint_address(push.mint), &expected.mint);
566 assert_eq!(push.bump, expected.bump);
567 assert_eq!(push.amount, expected.amount);
568 }
569 }
570
571 #[test]
572 fn finalize_settle_input_rejects_different_discriminator() {
573 let data = ix_data![
574 [SettlementInstruction::BeginSettle.discriminator()],
575 [0, 0], ];
577 let accounts: [AccountView; 0] = [];
578 assert_eq!(
579 FinalizeSettleInput::parse(&data, &accounts).err(),
580 Some(ProgramError::InvalidInstructionData),
581 );
582 }
583
584 #[test]
585 fn finalize_settle_input_rejects_empty_accounts() {
586 let data = ix_data![
587 [SettlementInstruction::FinalizeSettle.discriminator()],
588 [0, 0], ];
590 let accounts: [AccountView; 0] = [];
591 assert_eq!(
592 FinalizeSettleInput::parse(&data, &accounts).err(),
593 Some(ProgramError::NotEnoughAccountKeys),
594 );
595 }
596
597 #[test]
598 fn finalize_settle_input_rejects_account_count_mismatch() {
599 let data: Vec<u8> = ix_data![
602 [SettlementInstruction::FinalizeSettle.discriminator()],
603 [13, 37], [0xff], 31337u64.to_le_bytes(),
606 ];
607
608 let too_few = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 2 }>();
609 assert_eq!(
610 FinalizeSettleInput::parse(&data, &too_few).err(),
611 Some(SettlementError::AccountCountNotMatchingPushCount.into()),
612 );
613
614 let too_many = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 4 }>();
615 assert_eq!(
616 FinalizeSettleInput::parse(&data, &too_many).err(),
617 Some(SettlementError::AccountCountNotMatchingPushCount.into()),
618 );
619 }
620
621 #[test]
622 fn finalize_settle_input_rejects_partial_push() {
623 let accounts = fake_sequential_accounts::<FINALIZE_FIXED_ACCOUNTS>();
626 let data = ix_data![
627 [SettlementInstruction::FinalizeSettle.discriminator()],
628 [37, 13], [0xff], [0x11, 0x22, 0x33, 0x44], ];
632 assert_eq!(
633 FinalizeSettleInput::parse(&data, &accounts).err(),
634 Some(ProgramError::InvalidInstructionData),
635 );
636 }
637
638 #[test]
639 fn finalize_push_data_extracts_bumps_and_amounts() {
640 let bumps = [0xa1, 0xb1];
641 let amounts = [0x0102, 0x0304];
642 let ix = Instruction::from(FinalizeSettle {
643 program_id: pubkey_from_seed("program id"),
644 state_pda: pubkey_from_seed("state pda"),
645 begin_ix_index: 0x1337,
646 source_buffers: &[
647 pubkey_from_seed("source buffer 0"),
648 pubkey_from_seed("source buffer 1"),
649 ],
650 destinations: &[
651 pubkey_from_seed("destination 0"),
652 pubkey_from_seed("destination 1"),
653 ],
654 mints: &[None, None],
655 bumps: &bumps,
656 amounts: &amounts,
657 ..Default::default()
658 });
659
660 let (bumps, amounts) = finalize_push_data(&ix.data).expect("valid finalize data");
661 assert_eq!(bumps, [0xa1, 0xb1]);
662 assert_eq!(amounts, [0x0102u64.to_le_bytes(), 0x0304u64.to_le_bytes()]);
663 }
664
665 #[test]
666 fn finalize_push_data_handles_no_pushes() {
667 let ix = Instruction::from(FinalizeSettle {
668 program_id: pubkey_from_seed("program id"),
669 state_pda: pubkey_from_seed("state pda"),
670 ..Default::default()
671 });
672 let (bumps, amounts) = finalize_push_data(&ix.data).expect("valid empty finalize data");
673 assert!(bumps.is_empty());
674 assert!(amounts.is_empty());
675 }
676
677 #[test]
678 fn finalize_push_data_rejects_incorrect_bytes() {
679 let mut ix = Instruction::from(FinalizeSettle {
680 program_id: pubkey_from_seed("program id"),
681 state_pda: pubkey_from_seed("state pda"),
682 source_buffers: &[pubkey_from_seed("source buffer")],
683 destinations: &[pubkey_from_seed("destination")],
684 mints: &[None],
685 bumps: &[0xff],
686 amounts: &[31337],
687 ..Default::default()
688 });
689 ix.data.pop();
690 assert_eq!(
691 finalize_push_data(&ix.data).err(),
692 Some(ProgramError::InvalidInstructionData),
693 );
694 }
695
696 fn arb_finalize_instruction(
699 push_count: impl Into<prop::collection::SizeRange>,
700 ) -> impl Strategy<Value = Instruction> {
701 (
702 any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
703 any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
704 any::<u16>(),
705 crate::instruction::settle::fixtures::arb_pushes(push_count),
706 )
707 .prop_map(|(program_id, state_pda, begin_ix_index, pushes)| {
708 Instruction::from(FinalizeSettle {
709 program_id,
710 state_pda,
711 begin_ix_index,
712 source_buffers: &pushes.source_buffers,
713 destinations: &pushes.destinations,
714 mints: &pushes.mints,
715 bumps: &pushes.bumps,
716 amounts: &pushes.amounts,
717 ..Default::default()
718 })
719 })
720 }
721
722 proptest! {
723 #[test]
728 fn finalize_push_data_matches_parser(ix in arb_finalize_instruction(0..=16usize)) {
729 let (bumps, amounts) = finalize_push_data(&ix.data).expect("well-formed finalize data");
731 let recovered: Vec<(u8, u64)> = bumps
732 .iter()
733 .copied()
734 .zip(amounts.iter().copied().map(u64::from_le_bytes))
735 .collect();
736
737 let accounts: Vec<AccountView> =
739 ix.accounts.iter().map(|meta| fake_account(meta.pubkey)).collect();
740 let parsed = FinalizeSettleInput::parse(&ix.data, &accounts)
741 .expect("a well-formed finalize parses");
742 let parsed_pushes: Vec<(u8, u64)> =
743 parsed.pushes.iter().map(|push| (push.bump, push.amount)).collect();
744
745 prop_assert_eq!(recovered, parsed_pushes);
746 }
747 }
748}