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