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::{SettlementError, SettlementInstruction};
11
12use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID};
13
14#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct Pull {
18 pub destination: Pubkey,
19 pub amount: u64,
20}
21
22pub struct BeginSettle<'a> {
45 pub program_id: Pubkey,
46 pub state_pda: Pubkey,
47 pub finalize_ix_index: u16,
48 pub auction_id: i64,
52 pub order_pdas: &'a [Pubkey],
53 pub sell_token_accounts: &'a [Pubkey],
54 pub pulls: &'a [&'a [Pull]],
55}
56
57impl From<BeginSettle<'_>> for Instruction {
58 fn from(builder: BeginSettle<'_>) -> Self {
59 let BeginSettle {
60 program_id,
61 state_pda,
62 finalize_ix_index,
63 auction_id,
64 order_pdas,
65 sell_token_accounts,
66 pulls,
67 } = builder;
68
69 let mut order: Vec<usize> = (0..order_pdas.len()).collect();
73 order.sort_by_key(|&i| order_pdas[i]);
74
75 let counts: Vec<u8> = order.iter().map(|&i| pulls[i].len() as u8).collect();
76 let amounts: Vec<u8> = order
77 .iter()
78 .flat_map(|&i| pulls[i].iter())
79 .flat_map(|pull| pull.amount.to_le_bytes())
80 .collect();
81 let data = [
82 &[SettlementInstruction::BeginSettle.discriminator()][..],
83 &finalize_ix_index.to_le_bytes()[..],
84 &auction_id.to_le_bytes()[..],
85 &[order_pdas.len() as u8][..],
86 &counts[..],
87 &amounts[..],
88 ]
89 .concat();
90
91 let mut accounts = vec![
94 AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
95 AccountMeta::new_readonly(state_pda, false),
96 AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
97 ];
98 for &i in &order {
99 accounts.push(AccountMeta::new(order_pdas[i], false));
102 accounts.push(AccountMeta::new(sell_token_accounts[i], false));
105 for pull in pulls[i] {
106 accounts.push(AccountMeta::new(pull.destination, false));
107 }
108 }
109
110 Instruction {
111 program_id,
112 accounts,
113 data,
114 }
115 }
116}
117
118pub struct SettledOrder<'a, A> {
121 pub order_pda: &'a A,
122 pub sell_token_account: &'a A,
123 pub destinations: &'a [A],
125 pub amounts: &'a [[u8; 8]],
127}
128
129pub struct SettledOrders<'a, A> {
133 order_accounts: &'a [A],
139 counts: &'a [u8],
141 amounts: &'a [[u8; 8]],
144}
145
146impl<'a, A> SettledOrders<'a, A> {
147 #[allow(
149 clippy::arithmetic_side_effects,
150 reason = "offsets are bounded by tx limits"
151 )]
152 pub fn iter(&self) -> impl Iterator<Item = SettledOrder<'a, A>> + '_ {
153 let (counts, amounts) = (self.counts, self.amounts);
154 let mut rest: &'a [A] = self.order_accounts;
157 let mut i = 0usize;
158 let mut amount_offset = 0usize;
159 std::iter::from_fn(move || {
160 if i >= counts.len() {
161 return None;
162 }
163 let count = usize::from(counts[i]);
164 i += 1;
165
166 let (order_pda, tail) = rest.split_first()?;
167 let (sell_token_account, tail) = tail.split_first()?;
168 let (destinations, remainder) = tail.split_at(count);
169 rest = remainder;
170
171 let amount_end = amount_offset + count;
172 let order_amounts = &amounts[amount_offset..amount_end];
173 amount_offset = amount_end;
174
175 Some(SettledOrder {
176 order_pda,
177 sell_token_account,
178 destinations,
179 amounts: order_amounts,
180 })
181 })
182 }
183}
184
185pub struct BeginSettleInput<'a, A> {
192 pub finalize_ix_index: u16,
193 pub auction_id: i64,
197 pub instructions_sysvar_account: &'a A,
198 pub state_pda_account: &'a A,
199 pub token_program_account: &'a A,
200 pub orders: SettledOrders<'a, A>,
201}
202
203impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> {
207 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::BeginSettle;
208
209 fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
210 let (finalize_ix_index, body) = recover_counterpart(instruction_data)?;
211
212 let [instructions_sysvar_account, state_pda_account, token_program_account, order_accounts @ ..] =
213 accounts
214 else {
215 return Err(ProgramError::NotEnoughAccountKeys);
216 };
217
218 let (auction_id, body) = body
222 .split_first_chunk::<8>()
223 .ok_or(ProgramError::InvalidInstructionData)?;
224 let auction_id = i64::from_le_bytes(*auction_id);
225
226 let (&order_count, body) = body
232 .split_first()
233 .ok_or(ProgramError::InvalidInstructionData)?;
234 let order_count = usize::from(order_count);
235 let (counts, amount_bytes) = body
236 .split_at_checked(order_count)
237 .ok_or(ProgramError::InvalidInstructionData)?;
238 let (amounts, []) = amount_bytes.as_chunks::<8>() else {
239 return Err(ProgramError::InvalidInstructionData);
240 };
241 let transfer_count = amounts.len();
242
243 let expected_accounts = order_count
246 .checked_mul(2)
247 .and_then(|two_n| two_n.checked_add(transfer_count))
248 .ok_or(ProgramError::InvalidInstructionData)?;
249 if order_accounts.len() != expected_accounts {
250 return Err(SettlementError::AccountCountNotMatchingOrderCount.into());
251 }
252
253 let counts_sum: usize = counts.iter().map(|&c| usize::from(c)).sum();
257 if counts_sum != transfer_count {
258 return Err(SettlementError::TransferCountMismatch.into());
259 }
260
261 Ok(Self {
262 finalize_ix_index,
263 auction_id,
264 instructions_sysvar_account,
265 state_pda_account,
266 token_program_account,
267 orders: SettledOrders {
268 order_accounts,
269 counts,
270 amounts,
271 },
272 })
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::instruction::fixtures::{
280 fake_account, fake_account_from_array, fake_sequential_accounts,
281 };
282 use crate::instruction::settle::tests::ix_data;
283 use crate::instruction::tests::assert_readonly_nonsigner;
284 use hex_literal::hex;
285 use solana_account_view::AccountView;
286 use solana_address::Address;
287
288 const FIXED_ACCOUNTS: usize = 3;
291
292 const AUCTION_ID: i64 = 0x0102_0304_0506_0708;
295
296 #[test]
297 fn expected_encoding_begin_settle_no_orders() {
298 let program_id = Pubkey::new_unique();
299 let state_pda = Pubkey::new_unique();
300 let Instruction {
301 program_id: ix_program_id,
302 accounts,
303 data,
304 } = BeginSettle {
305 program_id,
306 state_pda,
307 finalize_ix_index: 0x1337,
308 auction_id: 0x0102_0304_0506_0708,
309 order_pdas: &[],
310 sell_token_accounts: &[],
311 pulls: &[],
312 }
313 .into();
314 assert_eq!(ix_program_id, program_id);
315 assert_eq!(
316 data,
317 ix_data![
318 [SettlementInstruction::BeginSettle.discriminator()],
319 hex!("3713"), hex!("0807060504030201"), [0], ],
323 );
324 assert_eq!(accounts.len(), 3);
329 assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID);
330 assert_readonly_nonsigner(&accounts[1], state_pda);
331 assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID);
332 }
333
334 #[test]
335 fn begin_settle_sorts_orders_by_pda() {
336 let program_id = Pubkey::new_unique();
337 let state_pda = Pubkey::new_unique();
338 let high_order_pda = Pubkey::new_from_array([0xbb; 32]);
341 let high_sell_token_account = Pubkey::new_from_array([0xa0; 32]);
342 let low_order_pda = Pubkey::new_from_array([0xaa; 32]);
343 let low_sell_token_account = Pubkey::new_from_array([0xb0; 32]);
344 let Instruction { data, accounts, .. } = BeginSettle {
345 program_id,
346 state_pda,
347 finalize_ix_index: 0x1337,
348 auction_id: AUCTION_ID,
349 order_pdas: &[high_order_pda, low_order_pda],
350 sell_token_accounts: &[high_sell_token_account, low_sell_token_account],
351 pulls: &[&[], &[]],
352 }
353 .into();
354
355 assert_eq!(
356 data,
357 ix_data![
358 [SettlementInstruction::BeginSettle.discriminator()],
359 hex!("3713"), AUCTION_ID.to_le_bytes(), [2], [0, 0], ],
364 );
365
366 let expected: Vec<Pubkey> = vec![
367 INSTRUCTIONS_SYSVAR_ID,
368 state_pda,
369 SPL_TOKEN_PROGRAM_ID,
370 low_order_pda,
371 low_sell_token_account,
372 high_order_pda,
373 high_sell_token_account,
374 ];
375 let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
376 assert_eq!(actual, expected);
377 let writable: Vec<Pubkey> = accounts
379 .iter()
380 .filter(|account| account.is_writable)
381 .map(|account| account.pubkey)
382 .collect();
383 assert_eq!(
384 writable,
385 vec![
386 low_order_pda,
387 low_sell_token_account,
388 high_order_pda,
389 high_sell_token_account,
390 ],
391 );
392 assert!(accounts.iter().all(|account| !account.is_signer));
393 }
394
395 #[test]
396 fn begin_settle_encodes_grouped_transfers() {
397 let program_id = Pubkey::new_unique();
398 let state_pda = Pubkey::new_unique();
399 let order_a = Pubkey::new_from_array([0x01; 32]);
400 let sell_a = Pubkey::new_from_array([0x02; 32]);
401 let order_b = Pubkey::new_from_array([0x03; 32]);
402 let sell_b = Pubkey::new_from_array([0x04; 32]);
403 let dest_a0 = Pubkey::new_from_array([0x05; 32]);
404 let dest_a1 = Pubkey::new_from_array([0x06; 32]);
405 let dest_b0 = Pubkey::new_from_array([0x07; 32]);
406
407 let Instruction { data, accounts, .. } = BeginSettle {
409 program_id,
410 state_pda,
411 finalize_ix_index: 0x1337,
412 auction_id: AUCTION_ID,
413 order_pdas: &[order_a, order_b],
414 sell_token_accounts: &[sell_a, sell_b],
415 pulls: &[
416 &[
417 Pull {
418 destination: dest_a0,
419 amount: 0x0102,
420 },
421 Pull {
422 destination: dest_a1,
423 amount: 0x0304,
424 },
425 ],
426 &[Pull {
427 destination: dest_b0,
428 amount: 0x0506,
429 }],
430 ],
431 }
432 .into();
433
434 assert_eq!(
435 data,
436 ix_data![
437 [SettlementInstruction::BeginSettle.discriminator()],
438 hex!("3713"), AUCTION_ID.to_le_bytes(), [2], [2, 1], hex!("0201000000000000"),
444 hex!("0403000000000000"),
445 hex!("0605000000000000"),
446 ],
447 );
448
449 let expected: Vec<Pubkey> = vec![
450 INSTRUCTIONS_SYSVAR_ID,
451 state_pda,
452 SPL_TOKEN_PROGRAM_ID,
453 order_a,
454 sell_a,
455 dest_a0,
456 dest_a1,
457 order_b,
458 sell_b,
459 dest_b0,
460 ];
461 let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
462 assert_eq!(actual, expected);
463 let writable: Vec<Pubkey> = accounts
465 .iter()
466 .filter(|account| account.is_writable)
467 .map(|account| account.pubkey)
468 .collect();
469 assert_eq!(
470 writable,
471 vec![order_a, sell_a, dest_a0, dest_a1, order_b, sell_b, dest_b0],
472 );
473 assert!(accounts.iter().all(|account| !account.is_signer));
474 }
475
476 #[test]
477 fn begin_settle_input_parses_valid_input() {
478 let sysvar = Address::new_from_array([0x42u8; 32]);
479 let state = Address::new_from_array([0x43u8; 32]);
481 let token_program = Address::new_from_array([0x44u8; 32]);
482 let accounts = [
483 fake_account(sysvar),
484 fake_account(state),
485 fake_account(token_program),
486 ];
487 let data = ix_data![
488 [SettlementInstruction::BeginSettle.discriminator()],
489 [0x37, 0x13], hex!("0807060504030201"), [0x00], ];
493 let BeginSettleInput {
494 finalize_ix_index,
495 auction_id,
496 instructions_sysvar_account,
497 orders,
498 token_program_account,
499 state_pda_account,
500 } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
501 assert_eq!(finalize_ix_index, 0x1337);
502 assert_eq!(auction_id, 0x0102_0304_0506_0708);
503 assert_eq!(instructions_sysvar_account.address(), &sysvar);
504 assert_eq!(orders.iter().count(), 0);
505 assert_eq!(token_program_account.address(), &token_program);
506 assert_eq!(state_pda_account.address(), &state);
507 }
508
509 #[test]
510 fn begin_settle_input_rejects_different_discriminator() {
511 let data = ix_data![
512 [SettlementInstruction::FinalizeSettle.discriminator()],
513 [0, 0], ];
515 let accounts: [AccountView; 0] = [];
516 assert_eq!(
517 BeginSettleInput::parse(&data, &accounts).err(),
518 Some(ProgramError::InvalidInstructionData),
519 );
520 }
521
522 #[test]
523 fn begin_settle_input_rejects_empty_accounts() {
524 let data = ix_data![
525 [SettlementInstruction::BeginSettle.discriminator()],
526 [0, 0], ];
528 let accounts: [AccountView; 0] = [];
529 assert_eq!(
530 BeginSettleInput::parse(&data, &accounts).err(),
531 Some(ProgramError::NotEnoughAccountKeys),
532 );
533 }
534
535 #[test]
536 fn begin_settle_input_rejects_missing_auction_id() {
537 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
540 let data = ix_data![
541 [SettlementInstruction::BeginSettle.discriminator()],
542 [0, 0], [0x01, 0x02, 0x03], ];
545 assert_eq!(
546 BeginSettleInput::parse(&data, &accounts).err(),
547 Some(ProgramError::InvalidInstructionData),
548 );
549 }
550
551 #[test]
552 fn begin_settle_input_pairs_orders_with_their_accounts() {
553 let sysvar = Address::new_from_array([1u8; 32]);
554 let state = Address::new_from_array([0xa1u8; 32]);
555 let token_program = Address::new_from_array([0xa2u8; 32]);
556 let order_pda = Address::new_from_array([2u8; 32]);
557 let sell_token = Address::new_from_array([3u8; 32]);
558 let accounts = [
559 fake_account(sysvar),
560 fake_account(state),
561 fake_account(token_program),
562 fake_account(order_pda),
563 fake_account(sell_token),
564 ];
565 let data = ix_data![
566 [SettlementInstruction::BeginSettle.discriminator()],
567 [0x37, 0x13], AUCTION_ID.to_le_bytes(), [0x01], [0x00], ];
572 let BeginSettleInput {
573 finalize_ix_index,
574 auction_id,
575 instructions_sysvar_account,
576 orders,
577 state_pda_account,
578 token_program_account,
579 } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
580 assert_eq!(finalize_ix_index, 0x1337);
581 assert_eq!(auction_id, AUCTION_ID);
582 assert_eq!(instructions_sysvar_account.address(), &sysvar);
583 assert_eq!(token_program_account.address(), &token_program);
584 assert_eq!(state_pda_account.address(), &state);
585
586 let mut orders = orders.iter();
587 let order = orders.next().expect("one settled order");
588 assert_eq!(order.order_pda.address(), &order_pda);
589 assert_eq!(order.sell_token_account.address(), &sell_token);
590 assert_eq!(order.destinations.len(), 0);
591 assert!(orders.next().is_none());
592 }
593
594 #[test]
595 fn begin_settle_input_parses_transfers() {
596 let sysvar = Address::new_from_array([1u8; 32]);
597 let state = Address::new_from_array([0xa1u8; 32]);
598 let token_program = Address::new_from_array([0xa2u8; 32]);
599 let order_pda = Address::new_from_array([2u8; 32]);
600 let sell_token = Address::new_from_array([3u8; 32]);
601 let dest0 = Address::new_from_array([4u8; 32]);
602 let dest1 = Address::new_from_array([5u8; 32]);
603 let accounts = [
604 fake_account(sysvar),
605 fake_account(state),
606 fake_account(token_program),
607 fake_account(order_pda),
608 fake_account(sell_token),
609 fake_account(dest0),
610 fake_account(dest1),
611 ];
612 let data = ix_data![
613 [SettlementInstruction::BeginSettle.discriminator()],
614 [0x37, 0x13], AUCTION_ID.to_le_bytes(), [0x01], [0x02], 0x1122u64.to_le_bytes(),
619 0x3344u64.to_le_bytes(),
620 ];
621
622 let BeginSettleInput { orders, .. } =
623 BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
624
625 let mut orders = orders.iter();
626 let order = orders.next().expect("one settled order");
627 assert_eq!(order.order_pda.address(), &order_pda);
628 assert_eq!(order.sell_token_account.address(), &sell_token);
629 let transfers: Vec<(&Address, u64)> = order
630 .destinations
631 .iter()
632 .zip(order.amounts)
633 .map(|(destination, amount)| (destination.address(), u64::from_le_bytes(*amount)))
634 .collect();
635 assert_eq!(transfers, vec![(&dest0, 0x1122), (&dest1, 0x3344)]);
636 assert!(orders.next().is_none());
637 }
638
639 #[test]
640 fn begin_settle_input_pairs_every_order_with_its_sell_token_account() {
641 const ORDER_COUNT: usize = 16;
642
643 let mut expected: Vec<(Address, Address)> = Vec::new();
644 for i in 0..ORDER_COUNT {
645 let order_pda = Address::new_from_array([i as u8; 32]);
646 let sell_token = Address::new_from_array([(i + ORDER_COUNT) as u8; 32]);
647 expected.push((order_pda, sell_token));
648 }
649
650 let mut accounts = vec![
653 fake_account_from_array([0xff; 32]),
654 fake_account_from_array([0xfe; 32]),
655 fake_account_from_array([0xfd; 32]),
656 ];
657 for &(order_pda, sell_token) in &expected {
658 accounts.push(fake_account(order_pda));
659 accounts.push(fake_account(sell_token));
660 }
661 let data = ix_data![
664 [SettlementInstruction::BeginSettle.discriminator()],
665 [0x37, 0x13], AUCTION_ID.to_le_bytes(), [ORDER_COUNT as u8], [0u8; ORDER_COUNT],
669 ];
670
671 let parsed = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
672
673 let actual: Vec<(Address, Address)> = parsed
674 .orders
675 .iter()
676 .map(|order| {
677 assert_eq!(order.destinations.len(), 0);
678 (
679 *order.order_pda.address(),
680 *order.sell_token_account.address(),
681 )
682 })
683 .collect();
684 assert_eq!(actual, expected);
685 }
686
687 #[test]
688 fn begin_settle_input_rejects_account_count_mismatch() {
689 let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 1 }>();
694 let data = ix_data![
695 [SettlementInstruction::BeginSettle.discriminator()],
696 [0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x00], ];
701 assert_eq!(
702 BeginSettleInput::parse(&data, &accounts).err(),
703 Some(SettlementError::AccountCountNotMatchingOrderCount.into()),
704 );
705 }
706
707 #[test]
708 fn begin_settle_input_rejects_counts_not_summing_to_destinations() {
709 let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 4 }>();
713 let data = ix_data![
714 [SettlementInstruction::BeginSettle.discriminator()],
715 [0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x01], 0u64.to_le_bytes(),
720 0u64.to_le_bytes(),
721 ];
722 assert_eq!(
723 BeginSettleInput::parse(&data, &accounts).err(),
724 Some(SettlementError::TransferCountMismatch.into()),
725 );
726 }
727
728 #[test]
729 fn begin_settle_input_rejects_missing_order_count() {
730 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
733 let data = ix_data![
734 [SettlementInstruction::BeginSettle.discriminator()],
735 [0, 0], AUCTION_ID.to_le_bytes(), ];
738 assert_eq!(
739 BeginSettleInput::parse(&data, &accounts).err(),
740 Some(ProgramError::InvalidInstructionData),
741 );
742 }
743
744 #[test]
745 fn begin_settle_input_rejects_body_too_short_for_counts() {
746 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
749 let data = ix_data![
750 [SettlementInstruction::BeginSettle.discriminator()],
751 [0, 0], AUCTION_ID.to_le_bytes(), [0x02], [0x00], ];
756 assert_eq!(
757 BeginSettleInput::parse(&data, &accounts).err(),
758 Some(ProgramError::InvalidInstructionData),
759 );
760 }
761
762 #[test]
763 fn begin_settle_input_rejects_partial_amount() {
764 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
767 let data = ix_data![
768 [SettlementInstruction::BeginSettle.discriminator()],
769 [0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x00], [0x11, 0x22, 0x33, 0x44], ];
775 assert_eq!(
776 BeginSettleInput::parse(&data, &accounts).err(),
777 Some(ProgramError::InvalidInstructionData),
778 );
779 }
780}