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