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, TokenProgram, INSTRUCTIONS_SYSVAR_ID};
13
14pub const FINALIZE_FIXED_ACCOUNTS: usize = 4;
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> {
96 pub program_id: Pubkey,
97 pub state_pda: Pubkey,
98 pub begin_ix_index: u16,
99 pub only_token_program: Option<TokenProgram>,
102 pub source_buffers: &'a [Pubkey],
103 pub destinations: &'a [Pubkey],
104 pub bumps: &'a [u8],
105 pub amounts: &'a [u64],
106}
107
108impl From<FinalizeSettle<'_>> for Instruction {
109 fn from(builder: FinalizeSettle<'_>) -> Self {
110 let FinalizeSettle {
111 program_id,
112 state_pda,
113 begin_ix_index,
114 only_token_program,
115 source_buffers,
116 destinations,
117 bumps,
118 amounts,
119 } = builder;
120
121 let data: Vec<u8> = core::iter::once(SettlementInstruction::FinalizeSettle.discriminator())
122 .chain(begin_ix_index.to_le_bytes())
123 .chain(bumps.iter().copied())
124 .chain(amounts.iter().flat_map(|amount| amount.to_le_bytes()))
125 .collect();
126
127 let mut accounts = vec![
128 AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
129 AccountMeta::new_readonly(state_pda, false),
130 ];
131 accounts.extend(TokenProgram::ALL.map(|program| {
135 let address = if only_token_program.is_none_or(|only| only == program) {
136 program.address()
137 } else {
138 INSTRUCTIONS_SYSVAR_ID
139 };
140 AccountMeta::new_readonly(address, false)
141 }));
142 for (source, destination) in source_buffers.iter().zip(destinations) {
143 accounts.push(AccountMeta::new(*source, false));
144 accounts.push(AccountMeta::new(*destination, false));
145 }
146
147 Instruction {
148 program_id,
149 accounts,
150 data,
151 }
152 }
153}
154
155#[derive(Debug, PartialEq, Eq)]
160pub struct Push<'a, A> {
161 pub source_buffer: &'a A,
162 pub destination: &'a A,
163 pub bump: u8,
164 pub amount: u64,
165}
166
167pub struct Pushes<'a, A> {
173 push_accounts: &'a [A],
175 bumps: &'a [u8],
176 amounts: &'a [[u8; 8]],
178}
179
180impl<'a, A> Pushes<'a, A> {
181 #[allow(
183 clippy::arithmetic_side_effects,
184 reason = "offsets are bounded by tx limits"
185 )]
186 pub fn iter(&self) -> impl Iterator<Item = Push<'a, A>> + '_ {
187 let push_count = self.bumps.len();
188 let mut i = 0usize;
189 let mut account_offset = 0usize;
190 std::iter::from_fn(move || {
191 if i >= push_count {
192 return None;
193 }
194 let bump = self.bumps[i];
195 let amount = u64::from_le_bytes(self.amounts[i]);
196 i += 1;
197
198 let source_buffer = &self.push_accounts[account_offset];
199 let destination = &self.push_accounts[account_offset + 1];
200 account_offset += 2;
201
202 Some(Push {
203 source_buffer,
204 destination,
205 bump,
206 amount,
207 })
208 })
209 }
210}
211
212pub struct FinalizeSettleInput<'a, A> {
220 pub begin_ix_index: u16,
221 pub instructions_sysvar_account: &'a A,
222 pub state_pda_account: &'a A,
223 pub pushes: Pushes<'a, A>,
224}
225
226impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> {
230 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::FinalizeSettle;
231
232 fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
233 let (begin_ix_index, body) = recover_counterpart(instruction_data)?;
234
235 let [instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, push_accounts @ ..] =
238 accounts
239 else {
240 return Err(ProgramError::NotEnoughAccountKeys);
241 };
242
243 let (bumps, amounts) = split_push_bytes(body)?;
244 let push_count = bumps.len();
245
246 let expected_accounts = push_count
249 .checked_mul(2)
250 .ok_or(ProgramError::InvalidInstructionData)?;
251 if push_accounts.len() != expected_accounts {
252 return Err(SettlementError::AccountCountNotMatchingPushCount.into());
253 }
254
255 Ok(Self {
256 begin_ix_index,
257 instructions_sysvar_account,
258 state_pda_account,
259 pushes: Pushes {
260 push_accounts,
261 bumps,
262 amounts,
263 },
264 })
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::fixtures::pubkey_from_seed;
272 use crate::instruction::fixtures::{
273 fake_account, fake_account_from_array, fake_sequential_accounts,
274 };
275 use crate::instruction::settle::tests::ix_data;
276 use crate::instruction::tests::assert_readonly_nonsigner;
277 use crate::token_program::TokenProgram;
278 use hex_literal::hex;
279 use proptest::prelude::*;
280 use solana_account_view::AccountView;
281 use solana_address::Address;
282
283 #[test]
284 fn finalize_fixed_accounts_matches_builder() {
285 let ix = Instruction::from(FinalizeSettle {
288 program_id: pubkey_from_seed("program id"),
289 state_pda: pubkey_from_seed("state pda"),
290 begin_ix_index: 0,
291 only_token_program: None,
292 source_buffers: &[],
293 destinations: &[],
294 bumps: &[],
295 amounts: &[],
296 });
297 assert_eq!(ix.accounts.len(), FINALIZE_FIXED_ACCOUNTS);
298 }
299
300 #[test]
301 fn expected_encoding_finalize_settle_no_pushes() {
302 let program_id = pubkey_from_seed("program id");
303 let state_pda = pubkey_from_seed("state pda");
304 let Instruction {
305 program_id: ix_program_id,
306 accounts,
307 data,
308 } = FinalizeSettle {
309 program_id,
310 state_pda,
311 begin_ix_index: 0x1337,
312 only_token_program: None,
313 source_buffers: &[],
314 destinations: &[],
315 bumps: &[],
316 amounts: &[],
317 }
318 .into();
319 assert_eq!(ix_program_id, program_id);
320 assert_eq!(
321 data,
322 ix_data![
323 [SettlementInstruction::FinalizeSettle.discriminator()],
324 hex!("3713"), ],
326 );
327 assert_eq!(accounts.len(), FINALIZE_FIXED_ACCOUNTS);
333 assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID);
334 assert_readonly_nonsigner(&accounts[1], state_pda);
335 assert_readonly_nonsigner(&accounts[2], TokenProgram::SplToken.address());
336 assert_readonly_nonsigner(&accounts[3], TokenProgram::Token2022.address());
337 }
338
339 #[test]
343 fn finalize_settle_carries_the_token_program_slots_it_is_given() {
344 for (only_token_program, expected) in [
345 (
346 None,
347 [
348 TokenProgram::SplToken.address(),
349 TokenProgram::Token2022.address(),
350 ],
351 ),
352 (
353 Some(TokenProgram::SplToken),
354 [TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID],
355 ),
356 (
357 Some(TokenProgram::Token2022),
358 [INSTRUCTIONS_SYSVAR_ID, TokenProgram::Token2022.address()],
359 ),
360 ] {
361 let ix = Instruction::from(FinalizeSettle {
362 program_id: Pubkey::new_unique(),
363 state_pda: Pubkey::new_unique(),
364 begin_ix_index: 0,
365 only_token_program,
366 source_buffers: &[],
367 destinations: &[],
368 bumps: &[],
369 amounts: &[],
370 });
371 let slots: Vec<Pubkey> = ix.accounts[2..].iter().map(|meta| meta.pubkey).collect();
372 assert_eq!(
373 slots, expected,
374 "{only_token_program:?} should name just the programs it settles against",
375 );
376 }
377 }
378
379 #[test]
380 fn finalize_settle_encodes_pushes() {
381 let program_id = pubkey_from_seed("program id");
382 let state_pda = pubkey_from_seed("state pda");
383 let source_a = Pubkey::new_from_array([0x01; 32]);
384 let dest_a = Pubkey::new_from_array([0x02; 32]);
385 let source_b = Pubkey::new_from_array([0x03; 32]);
386 let dest_b = Pubkey::new_from_array([0x04; 32]);
387
388 let ix = Instruction::from(FinalizeSettle {
389 program_id,
390 state_pda,
391 begin_ix_index: 0x1337,
392 only_token_program: None,
393 source_buffers: &[source_a, source_b],
394 destinations: &[dest_a, dest_b],
395 bumps: &[0xa1, 0xb1],
396 amounts: &[0x01020304, 0x05060708],
397 });
398
399 assert_eq!(
400 ix.data,
401 ix_data![
402 [SettlementInstruction::FinalizeSettle.discriminator()],
403 hex!("3713"), [0xa1, 0xb1], hex!("0403020100000000"),
407 hex!("0807060500000000"),
408 ],
409 );
410
411 let actual: Vec<Pubkey> = ix.accounts.iter().map(|meta| meta.pubkey).collect();
412 assert_eq!(
413 actual,
414 vec![
415 INSTRUCTIONS_SYSVAR_ID,
416 state_pda,
417 TokenProgram::SplToken.address(),
418 TokenProgram::Token2022.address(),
419 source_a,
420 dest_a,
421 source_b,
422 dest_b,
423 ],
424 );
425 let writable: Vec<Pubkey> = ix
428 .accounts
429 .iter()
430 .filter(|meta| meta.is_writable)
431 .map(|meta| meta.pubkey)
432 .collect();
433 assert_eq!(writable, vec![source_a, dest_a, source_b, dest_b]);
434 assert!(ix.accounts.iter().all(|meta| !meta.is_signer));
435 }
436
437 #[test]
438 fn finalize_settle_input_parses_no_pushes() {
439 let sysvar = pubkey_from_seed("sysvar");
440 let state = pubkey_from_seed("state pda");
441 let spl_token_program = pubkey_from_seed("spl token program");
442 let token_2022_program = pubkey_from_seed("token 2022 program");
443 let accounts = [
444 fake_account(sysvar),
445 fake_account(state),
446 fake_account(spl_token_program),
447 fake_account(token_2022_program),
448 ];
449 let data = ix_data![
450 [SettlementInstruction::FinalizeSettle.discriminator()],
451 [0x37, 0x13], ];
453 let FinalizeSettleInput {
454 begin_ix_index,
455 instructions_sysvar_account,
456 state_pda_account,
457 pushes,
458 } = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
459 assert_eq!(begin_ix_index, 0x1337);
460 assert_eq!(instructions_sysvar_account.address(), &sysvar);
461 assert_eq!(state_pda_account.address(), &state);
462 assert_eq!(pushes.iter().count(), 0);
463 }
464
465 #[test]
466 fn finalize_settle_input_parses_pushes() {
467 let sysvar = pubkey_from_seed("sysvar");
468 let state = pubkey_from_seed("state pda");
469 let spl_token_program = pubkey_from_seed("spl token program");
470 let token_2022_program = pubkey_from_seed("token 2022 program");
471 let source = pubkey_from_seed("source buffer");
474 let dest0 = pubkey_from_seed("destination 0");
475 let dest1 = pubkey_from_seed("destination 1");
476 let accounts = [
477 fake_account(sysvar),
478 fake_account(state),
479 fake_account(spl_token_program),
480 fake_account(token_2022_program),
481 fake_account(source),
482 fake_account(dest0),
483 fake_account(source),
484 fake_account(dest1),
485 ];
486 let data = ix_data![
487 [SettlementInstruction::FinalizeSettle.discriminator()],
488 [0x13, 0x37], [0xfe, 0xfd], 0x1122u64.to_le_bytes(),
491 0x3344u64.to_le_bytes(),
492 ];
493
494 let FinalizeSettleInput { pushes, .. } =
495 FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
496
497 let parsed: Vec<(&Address, &Address, u8, u64)> = pushes
498 .iter()
499 .map(|push| {
500 (
501 push.source_buffer.address(),
502 push.destination.address(),
503 push.bump,
504 push.amount,
505 )
506 })
507 .collect();
508 assert_eq!(
509 parsed,
510 vec![
511 (&source, &dest0, 0xfe, 0x1122),
512 (&source, &dest1, 0xfd, 0x3344),
513 ],
514 );
515 }
516
517 #[test]
518 fn finalize_settle_input_parses_many_pushes() {
519 const PUSH_COUNT: usize = 16;
520
521 struct ExpectedPush {
522 source: Address,
523 dest: Address,
524 bump: u8,
525 amount: u64,
526 }
527 let mut expected: Vec<ExpectedPush> = Vec::new();
528 for i in 0..PUSH_COUNT {
529 let source = pubkey_from_seed(&format!("source buffer {i}"));
530 let dest = pubkey_from_seed(&format!("destination {i}"));
531 let bump = (i + 2 * PUSH_COUNT) as u8;
532 let amount = u64::from_le_bytes([(i + 3 * PUSH_COUNT) as u8; 8]);
533 expected.push(ExpectedPush {
534 source,
535 dest,
536 bump,
537 amount,
538 });
539 }
540
541 let mut accounts = vec![
544 fake_account_from_array([0xff; 32]),
545 fake_account_from_array([0xfe; 32]),
546 fake_account_from_array([0xfd; 32]),
547 fake_account_from_array([0xfc; 32]),
548 ];
549 let mut bump_bytes = Vec::new();
550 let mut amount_bytes = Vec::new();
551 for push in &expected {
552 accounts.push(fake_account(push.source));
553 accounts.push(fake_account(push.dest));
554 bump_bytes.push(push.bump);
555 amount_bytes.extend_from_slice(&push.amount.to_le_bytes());
556 }
557 let data = ix_data![
558 [SettlementInstruction::FinalizeSettle.discriminator()],
559 [0x13, 0x37], bump_bytes,
561 amount_bytes,
562 ];
563
564 let parsed = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
565 let pushes: Vec<_> = parsed.pushes.iter().collect();
566
567 assert_eq!(pushes.len(), PUSH_COUNT);
568 for (push, expected) in pushes.iter().zip(&expected) {
569 assert_eq!(push.source_buffer.address(), &expected.source);
570 assert_eq!(push.destination.address(), &expected.dest);
571 assert_eq!(push.bump, expected.bump);
572 assert_eq!(push.amount, expected.amount);
573 }
574 }
575
576 #[test]
577 fn finalize_settle_input_rejects_different_discriminator() {
578 let data = ix_data![
579 [SettlementInstruction::BeginSettle.discriminator()],
580 [0, 0], ];
582 let accounts: [AccountView; 0] = [];
583 assert_eq!(
584 FinalizeSettleInput::parse(&data, &accounts).err(),
585 Some(ProgramError::InvalidInstructionData),
586 );
587 }
588
589 #[test]
590 fn finalize_settle_input_rejects_empty_accounts() {
591 let data = ix_data![
592 [SettlementInstruction::FinalizeSettle.discriminator()],
593 [0, 0], ];
595 let accounts: [AccountView; 0] = [];
596 assert_eq!(
597 FinalizeSettleInput::parse(&data, &accounts).err(),
598 Some(ProgramError::NotEnoughAccountKeys),
599 );
600 }
601
602 #[test]
603 fn finalize_settle_input_rejects_account_count_mismatch() {
604 let data: Vec<u8> = ix_data![
607 [SettlementInstruction::FinalizeSettle.discriminator()],
608 [13, 37], [0xff], 31337u64.to_le_bytes(),
611 ];
612
613 let too_few = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 1 }>();
615 assert_eq!(
616 FinalizeSettleInput::parse(&data, &too_few).err(),
617 Some(SettlementError::AccountCountNotMatchingPushCount.into()),
618 );
619
620 let too_many = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 3 }>();
622 assert_eq!(
623 FinalizeSettleInput::parse(&data, &too_many).err(),
624 Some(SettlementError::AccountCountNotMatchingPushCount.into()),
625 );
626 }
627
628 #[test]
629 fn finalize_settle_input_rejects_partial_push() {
630 let accounts = fake_sequential_accounts::<FINALIZE_FIXED_ACCOUNTS>();
633 let data = ix_data![
634 [SettlementInstruction::FinalizeSettle.discriminator()],
635 [37, 13], [0xff], [0x11, 0x22, 0x33, 0x44], ];
639 assert_eq!(
640 FinalizeSettleInput::parse(&data, &accounts).err(),
641 Some(ProgramError::InvalidInstructionData),
642 );
643 }
644
645 #[test]
646 fn finalize_push_data_extracts_bumps_and_amounts() {
647 let bumps = [0xa1, 0xb1];
648 let amounts = [0x0102, 0x0304];
649 let ix = Instruction::from(FinalizeSettle {
650 program_id: pubkey_from_seed("program id"),
651 state_pda: pubkey_from_seed("state pda"),
652 begin_ix_index: 0x1337,
653 only_token_program: None,
654 source_buffers: &[
655 pubkey_from_seed("source buffer 0"),
656 pubkey_from_seed("source buffer 1"),
657 ],
658 destinations: &[
659 pubkey_from_seed("destination 0"),
660 pubkey_from_seed("destination 1"),
661 ],
662 bumps: &bumps,
663 amounts: &amounts,
664 });
665
666 let recovered = finalize_push_data(&ix.data).expect("valid finalize data");
667 let decoded: Vec<(u8, u64)> = recovered.collect();
668 assert_eq!(decoded, [(0xa1, 0x0102), (0xb1, 0x0304)]);
669 }
670
671 #[test]
672 fn finalize_push_data_handles_no_pushes() {
673 let ix = Instruction::from(FinalizeSettle {
674 program_id: pubkey_from_seed("program id"),
675 state_pda: pubkey_from_seed("state pda"),
676 begin_ix_index: 0,
677 only_token_program: None,
678 source_buffers: &[],
679 destinations: &[],
680 bumps: &[],
681 amounts: &[],
682 });
683 let mut pushes = finalize_push_data(&ix.data).expect("valid empty finalize data");
684 assert!(pushes.next().is_none());
685 }
686
687 #[test]
688 fn finalize_push_data_rejects_incorrect_bytes() {
689 let mut ix = Instruction::from(FinalizeSettle {
690 program_id: pubkey_from_seed("program id"),
691 state_pda: pubkey_from_seed("state pda"),
692 begin_ix_index: 0,
693 only_token_program: None,
694 source_buffers: &[pubkey_from_seed("source buffer")],
695 destinations: &[pubkey_from_seed("destination")],
696 bumps: &[0xff],
697 amounts: &[31337],
698 });
699 ix.data.pop();
700 assert_eq!(
701 finalize_push_data(&ix.data).err(),
702 Some(ProgramError::InvalidInstructionData),
703 );
704 }
705
706 fn arb_finalize_instruction(
709 push_count: impl Into<prop::collection::SizeRange>,
710 ) -> impl Strategy<Value = Instruction> {
711 (
712 any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
713 any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
714 any::<u16>(),
715 crate::instruction::settle::fixtures::arb_pushes(push_count),
716 )
717 .prop_map(
718 |(
719 program_id,
720 state_pda,
721 begin_ix_index,
722 (source_buffers, destinations, bumps, amounts),
723 )| {
724 Instruction::from(FinalizeSettle {
725 program_id,
726 state_pda,
727 begin_ix_index,
728 only_token_program: None,
729 source_buffers: &source_buffers,
730 destinations: &destinations,
731 bumps: &bumps,
732 amounts: &amounts,
733 })
734 },
735 )
736 }
737
738 proptest! {
739 #[test]
744 fn finalize_push_data_matches_parser(ix in arb_finalize_instruction(0..=16usize)) {
745 let recovered: Vec<(u8, u64)> =
747 finalize_push_data(&ix.data).expect("well-formed finalize data").collect();
748
749 let accounts: Vec<AccountView> =
751 ix.accounts.iter().map(|meta| fake_account(meta.pubkey)).collect();
752 let parsed = FinalizeSettleInput::parse(&ix.data, &accounts)
753 .expect("a well-formed finalize parses");
754 let parsed_pushes: Vec<(u8, u64)> =
755 parsed.pushes.iter().map(|push| (push.bump, push.amount)).collect();
756
757 prop_assert_eq!(recovered, parsed_pushes);
758 }
759 }
760}