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> {
47 pub program_id: Pubkey,
48 pub state_pda: Pubkey,
49 pub solver: Pubkey,
50 pub finalize_ix_index: u16,
51 pub auction_id: i64,
55 pub order_pdas: &'a [Pubkey],
56 pub sell_token_accounts: &'a [Pubkey],
57 pub pulls: &'a [&'a [Pull]],
58}
59
60impl From<BeginSettle<'_>> for Instruction {
61 fn from(builder: BeginSettle<'_>) -> Self {
62 let BeginSettle {
63 program_id,
64 state_pda,
65 solver,
66 finalize_ix_index,
67 auction_id,
68 order_pdas,
69 sell_token_accounts,
70 pulls,
71 } = builder;
72
73 let mut order: Vec<usize> = (0..order_pdas.len()).collect();
77 order.sort_by_key(|&i| order_pdas[i]);
78
79 let counts: Vec<u8> = order.iter().map(|&i| pulls[i].len() as u8).collect();
80 let amounts: Vec<u8> = order
81 .iter()
82 .flat_map(|&i| pulls[i].iter())
83 .flat_map(|pull| pull.amount.to_le_bytes())
84 .collect();
85 let data = [
86 &[SettlementInstruction::BeginSettle.discriminator()][..],
87 &finalize_ix_index.to_le_bytes()[..],
88 &auction_id.to_le_bytes()[..],
89 &[order_pdas.len() as u8][..],
90 &counts[..],
91 &amounts[..],
92 ]
93 .concat();
94
95 let mut accounts = vec![
98 AccountMeta::new_readonly(solver, true),
99 AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
100 AccountMeta::new_readonly(state_pda, false),
101 AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false),
102 ];
103 for &i in &order {
104 accounts.push(AccountMeta::new(order_pdas[i], false));
107 accounts.push(AccountMeta::new(sell_token_accounts[i], false));
110 for pull in pulls[i] {
111 accounts.push(AccountMeta::new(pull.destination, false));
112 }
113 }
114
115 Instruction {
116 program_id,
117 accounts,
118 data,
119 }
120 }
121}
122
123pub struct SettledOrder<'a, A> {
126 pub order_pda: &'a A,
127 pub sell_token_account: &'a A,
128 pub destinations: &'a [A],
130 pub amounts: &'a [[u8; 8]],
132}
133
134pub struct SettledOrders<'a, A> {
138 order_accounts: &'a [A],
144 counts: &'a [u8],
146 amounts: &'a [[u8; 8]],
149}
150
151impl<'a, A> SettledOrders<'a, A> {
152 #[allow(
154 clippy::arithmetic_side_effects,
155 reason = "offsets are bounded by tx limits"
156 )]
157 pub fn iter(&self) -> impl Iterator<Item = SettledOrder<'a, A>> + '_ {
158 let (counts, amounts) = (self.counts, self.amounts);
159 let mut rest: &'a [A] = self.order_accounts;
162 let mut i = 0usize;
163 let mut amount_offset = 0usize;
164 std::iter::from_fn(move || {
165 if i >= counts.len() {
166 return None;
167 }
168 let count = usize::from(counts[i]);
169 i += 1;
170
171 let (order_pda, tail) = rest.split_first()?;
172 let (sell_token_account, tail) = tail.split_first()?;
173 let (destinations, remainder) = tail.split_at(count);
174 rest = remainder;
175
176 let amount_end = amount_offset + count;
177 let order_amounts = &amounts[amount_offset..amount_end];
178 amount_offset = amount_end;
179
180 Some(SettledOrder {
181 order_pda,
182 sell_token_account,
183 destinations,
184 amounts: order_amounts,
185 })
186 })
187 }
188}
189
190pub struct BeginSettleInput<'a, A> {
197 pub finalize_ix_index: u16,
198 pub auction_id: i64,
202 pub solver_account: &'a A,
203 pub instructions_sysvar_account: &'a A,
204 pub state_pda_account: &'a A,
205 pub token_program_account: &'a A,
206 pub orders: SettledOrders<'a, A>,
207}
208
209impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> {
213 const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::BeginSettle;
214
215 fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
216 let (finalize_ix_index, body) = recover_counterpart(instruction_data)?;
217
218 let [solver_account, instructions_sysvar_account, state_pda_account, token_program_account, order_accounts @ ..] =
219 accounts
220 else {
221 return Err(ProgramError::NotEnoughAccountKeys);
222 };
223
224 let (auction_id, body) = body
228 .split_first_chunk::<8>()
229 .ok_or(ProgramError::InvalidInstructionData)?;
230 let auction_id = i64::from_le_bytes(*auction_id);
231
232 let (&order_count, body) = body
238 .split_first()
239 .ok_or(ProgramError::InvalidInstructionData)?;
240 let order_count = usize::from(order_count);
241 let (counts, amount_bytes) = body
242 .split_at_checked(order_count)
243 .ok_or(ProgramError::InvalidInstructionData)?;
244 let (amounts, []) = amount_bytes.as_chunks::<8>() else {
245 return Err(ProgramError::InvalidInstructionData);
246 };
247 let transfer_count = amounts.len();
248
249 let expected_accounts = order_count
252 .checked_mul(2)
253 .and_then(|two_n| two_n.checked_add(transfer_count))
254 .ok_or(ProgramError::InvalidInstructionData)?;
255 if order_accounts.len() != expected_accounts {
256 return Err(SettlementError::AccountCountNotMatchingOrderCount.into());
257 }
258
259 let counts_sum: usize = counts.iter().map(|&c| usize::from(c)).sum();
263 if counts_sum != transfer_count {
264 return Err(SettlementError::TransferCountMismatch.into());
265 }
266
267 Ok(Self {
268 finalize_ix_index,
269 auction_id,
270 instructions_sysvar_account,
271 state_pda_account,
272 token_program_account,
273 solver_account,
274 orders: SettledOrders {
275 order_accounts,
276 counts,
277 amounts,
278 },
279 })
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::fixtures::pubkey_from_seed;
287 use crate::instruction::fixtures::{
288 fake_account, fake_account_from_array, fake_sequential_accounts,
289 };
290 use crate::instruction::settle::tests::ix_data;
291 use crate::instruction::tests::{assert_readonly_nonsigner, assert_readonly_signer};
292 use hex_literal::hex;
293 use solana_account_view::AccountView;
294 use solana_address::Address;
295
296 const FIXED_ACCOUNTS: usize = 4;
300
301 const AUCTION_ID: i64 = 0x0102_0304_0506_0708;
304
305 #[test]
306 fn expected_encoding_begin_settle_no_orders() {
307 let program_id = pubkey_from_seed("program id");
308 let state_pda = pubkey_from_seed("state pda");
309 let solver = pubkey_from_seed("solver");
310 let Instruction {
311 program_id: ix_program_id,
312 accounts,
313 data,
314 } = BeginSettle {
315 program_id,
316 state_pda,
317 solver,
318 finalize_ix_index: 0x1337,
319 auction_id: 0x0102_0304_0506_0708,
320 order_pdas: &[],
321 sell_token_accounts: &[],
322 pulls: &[],
323 }
324 .into();
325 assert_eq!(ix_program_id, program_id);
326 assert_eq!(
327 data,
328 ix_data![
329 [SettlementInstruction::BeginSettle.discriminator()],
330 hex!("3713"), hex!("0807060504030201"), [0], ],
334 );
335 assert_eq!(accounts.len(), 4);
339 assert_readonly_signer(&accounts[0], solver);
340 assert_readonly_nonsigner(&accounts[1], INSTRUCTIONS_SYSVAR_ID);
341 assert_readonly_nonsigner(&accounts[2], state_pda);
342 assert_readonly_nonsigner(&accounts[3], SPL_TOKEN_PROGRAM_ID);
343 }
344
345 #[test]
346 fn begin_settle_sorts_orders_by_pda() {
347 let program_id = pubkey_from_seed("program id");
348 let state_pda = pubkey_from_seed("state pda");
349 let solver = pubkey_from_seed("solver");
350 let high_order_pda = Pubkey::new_from_array([0xbb; 32]);
353 let high_sell_token_account = Pubkey::new_from_array([0xa0; 32]);
354 let low_order_pda = Pubkey::new_from_array([0xaa; 32]);
355 let low_sell_token_account = Pubkey::new_from_array([0xb0; 32]);
356 let Instruction { data, accounts, .. } = BeginSettle {
357 program_id,
358 state_pda,
359 solver,
360 finalize_ix_index: 0x1337,
361 auction_id: AUCTION_ID,
362 order_pdas: &[high_order_pda, low_order_pda],
363 sell_token_accounts: &[high_sell_token_account, low_sell_token_account],
364 pulls: &[&[], &[]],
365 }
366 .into();
367
368 assert_eq!(
369 data,
370 ix_data![
371 [SettlementInstruction::BeginSettle.discriminator()],
372 hex!("3713"), AUCTION_ID.to_le_bytes(), [2], [0, 0], ],
377 );
378
379 let expected: Vec<Pubkey> = vec![
380 solver,
381 INSTRUCTIONS_SYSVAR_ID,
382 state_pda,
383 SPL_TOKEN_PROGRAM_ID,
384 low_order_pda,
385 low_sell_token_account,
386 high_order_pda,
387 high_sell_token_account,
388 ];
389 let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
390 assert_eq!(actual, expected);
391 let writable: Vec<Pubkey> = accounts
393 .iter()
394 .filter(|account| account.is_writable)
395 .map(|account| account.pubkey)
396 .collect();
397 assert_eq!(
398 writable,
399 vec![
400 low_order_pda,
401 low_sell_token_account,
402 high_order_pda,
403 high_sell_token_account,
404 ],
405 );
406 let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS);
408 assert!(order_accounts.iter().all(|account| !account.is_signer));
409 }
410
411 #[test]
412 fn begin_settle_encodes_grouped_transfers() {
413 let program_id = pubkey_from_seed("program id");
414 let state_pda = pubkey_from_seed("state pda");
415 let solver = pubkey_from_seed("solver");
416 let order_a = Pubkey::new_from_array([0x01; 32]);
417 let sell_a = Pubkey::new_from_array([0x02; 32]);
418 let order_b = Pubkey::new_from_array([0x03; 32]);
419 let sell_b = Pubkey::new_from_array([0x04; 32]);
420 let dest_a0 = Pubkey::new_from_array([0x05; 32]);
421 let dest_a1 = Pubkey::new_from_array([0x06; 32]);
422 let dest_b0 = Pubkey::new_from_array([0x07; 32]);
423
424 let Instruction { data, accounts, .. } = BeginSettle {
426 program_id,
427 state_pda,
428 solver,
429 finalize_ix_index: 0x1337,
430 auction_id: AUCTION_ID,
431 order_pdas: &[order_a, order_b],
432 sell_token_accounts: &[sell_a, sell_b],
433 pulls: &[
434 &[
435 Pull {
436 destination: dest_a0,
437 amount: 0x0102,
438 },
439 Pull {
440 destination: dest_a1,
441 amount: 0x0304,
442 },
443 ],
444 &[Pull {
445 destination: dest_b0,
446 amount: 0x0506,
447 }],
448 ],
449 }
450 .into();
451
452 assert_eq!(
453 data,
454 ix_data![
455 [SettlementInstruction::BeginSettle.discriminator()],
456 hex!("3713"), AUCTION_ID.to_le_bytes(), [2], [2, 1], hex!("0201000000000000"),
462 hex!("0403000000000000"),
463 hex!("0605000000000000"),
464 ],
465 );
466
467 let expected: Vec<Pubkey> = vec![
468 solver,
469 INSTRUCTIONS_SYSVAR_ID,
470 state_pda,
471 SPL_TOKEN_PROGRAM_ID,
472 order_a,
473 sell_a,
474 dest_a0,
475 dest_a1,
476 order_b,
477 sell_b,
478 dest_b0,
479 ];
480 let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
481 assert_eq!(actual, expected);
482 let writable: Vec<Pubkey> = accounts
484 .iter()
485 .filter(|account| account.is_writable)
486 .map(|account| account.pubkey)
487 .collect();
488 assert_eq!(
489 writable,
490 vec![order_a, sell_a, dest_a0, dest_a1, order_b, sell_b, dest_b0],
491 );
492 let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS);
494 assert!(order_accounts.iter().all(|account| !account.is_signer));
495 }
496
497 #[test]
498 fn begin_settle_input_parses_valid_input() {
499 let sysvar = pubkey_from_seed("sysvar");
500 let state = pubkey_from_seed("state pda");
501 let token_program = pubkey_from_seed("token program");
502 let solver = pubkey_from_seed("solver");
503 let accounts = [
504 fake_account(solver),
505 fake_account(sysvar),
506 fake_account(state),
507 fake_account(token_program),
508 ];
509 let data = ix_data![
510 [SettlementInstruction::BeginSettle.discriminator()],
511 [0x37, 0x13], hex!("0807060504030201"), [0x00], ];
515 let BeginSettleInput {
516 finalize_ix_index,
517 auction_id,
518 solver_account,
519 instructions_sysvar_account,
520 orders,
521 token_program_account,
522 state_pda_account,
523 } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
524 assert_eq!(finalize_ix_index, 0x1337);
525 assert_eq!(auction_id, 0x0102_0304_0506_0708);
526 assert_eq!(instructions_sysvar_account.address(), &sysvar);
527 assert_eq!(orders.iter().count(), 0);
528 assert_eq!(token_program_account.address(), &token_program);
529 assert_eq!(state_pda_account.address(), &state);
530 assert_eq!(solver_account.address(), &solver);
531 }
532
533 #[test]
534 fn begin_settle_input_rejects_different_discriminator() {
535 let data = ix_data![
536 [SettlementInstruction::FinalizeSettle.discriminator()],
537 [0, 0], ];
539 let accounts: [AccountView; 0] = [];
540 assert_eq!(
541 BeginSettleInput::parse(&data, &accounts).err(),
542 Some(ProgramError::InvalidInstructionData),
543 );
544 }
545
546 #[test]
547 fn begin_settle_input_rejects_empty_accounts() {
548 let data = ix_data![
549 [SettlementInstruction::BeginSettle.discriminator()],
550 [0, 0], ];
552 let accounts: [AccountView; 0] = [];
553 assert_eq!(
554 BeginSettleInput::parse(&data, &accounts).err(),
555 Some(ProgramError::NotEnoughAccountKeys),
556 );
557 }
558
559 #[test]
560 fn begin_settle_input_rejects_missing_auction_id() {
561 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
564 let data = ix_data![
565 [SettlementInstruction::BeginSettle.discriminator()],
566 [0, 0], [0x01, 0x02, 0x03], ];
569 assert_eq!(
570 BeginSettleInput::parse(&data, &accounts).err(),
571 Some(ProgramError::InvalidInstructionData),
572 );
573 }
574
575 #[test]
576 fn begin_settle_input_pairs_orders_with_their_accounts() {
577 let sysvar = pubkey_from_seed("sysvar");
578 let state = pubkey_from_seed("state pda");
579 let token_program = pubkey_from_seed("token program");
580 let solver = pubkey_from_seed("solver");
581 let order_pda = pubkey_from_seed("order pda");
582 let sell_token = pubkey_from_seed("sell token");
583 let accounts = [
584 fake_account(solver),
585 fake_account(sysvar),
586 fake_account(state),
587 fake_account(token_program),
588 fake_account(order_pda),
589 fake_account(sell_token),
590 ];
591 let data = ix_data![
592 [SettlementInstruction::BeginSettle.discriminator()],
593 [0x37, 0x13], AUCTION_ID.to_le_bytes(), [0x01], [0x00], ];
598 let BeginSettleInput {
599 finalize_ix_index,
600 auction_id,
601 solver_account,
602 instructions_sysvar_account,
603 orders,
604 state_pda_account,
605 token_program_account,
606 } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
607 assert_eq!(finalize_ix_index, 0x1337);
608 assert_eq!(auction_id, AUCTION_ID);
609 assert_eq!(instructions_sysvar_account.address(), &sysvar);
610 assert_eq!(token_program_account.address(), &token_program);
611 assert_eq!(state_pda_account.address(), &state);
612 assert_eq!(solver_account.address(), &solver);
613
614 let mut orders = orders.iter();
615 let order = orders.next().expect("one settled order");
616 assert_eq!(order.order_pda.address(), &order_pda);
617 assert_eq!(order.sell_token_account.address(), &sell_token);
618 assert_eq!(order.destinations.len(), 0);
619 assert!(orders.next().is_none());
620 }
621
622 #[test]
623 fn begin_settle_input_parses_transfers() {
624 let sysvar = pubkey_from_seed("sysvar");
625 let state = pubkey_from_seed("state pda");
626 let token_program = pubkey_from_seed("token program");
627 let solver = pubkey_from_seed("solver");
628 let order_pda = pubkey_from_seed("order pda");
629 let sell_token = pubkey_from_seed("sell token");
630 let dest0 = pubkey_from_seed("destination 0");
631 let dest1 = pubkey_from_seed("destination 1");
632 let accounts = [
633 fake_account(solver),
634 fake_account(sysvar),
635 fake_account(state),
636 fake_account(token_program),
637 fake_account(order_pda),
638 fake_account(sell_token),
639 fake_account(dest0),
640 fake_account(dest1),
641 ];
642 let data = ix_data![
643 [SettlementInstruction::BeginSettle.discriminator()],
644 [0x37, 0x13], AUCTION_ID.to_le_bytes(), [0x01], [0x02], 0x1122u64.to_le_bytes(),
649 0x3344u64.to_le_bytes(),
650 ];
651
652 let BeginSettleInput { orders, .. } =
653 BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
654
655 let mut orders = orders.iter();
656 let order = orders.next().expect("one settled order");
657 assert_eq!(order.order_pda.address(), &order_pda);
658 assert_eq!(order.sell_token_account.address(), &sell_token);
659 let transfers: Vec<(&Address, u64)> = order
660 .destinations
661 .iter()
662 .zip(order.amounts)
663 .map(|(destination, amount)| (destination.address(), u64::from_le_bytes(*amount)))
664 .collect();
665 assert_eq!(transfers, vec![(&dest0, 0x1122), (&dest1, 0x3344)]);
666 assert!(orders.next().is_none());
667 }
668
669 #[test]
670 fn begin_settle_input_pairs_every_order_with_its_sell_token_account() {
671 const ORDER_COUNT: usize = 16;
672
673 let mut expected: Vec<(Address, Address)> = Vec::new();
674 for i in 0..ORDER_COUNT {
675 let order_pda = pubkey_from_seed(&format!("order pda {i}"));
676 let sell_token = pubkey_from_seed(&format!("sell token {i}"));
677 expected.push((order_pda, sell_token));
678 }
679
680 let mut accounts = vec![
683 fake_account_from_array([0xff; 32]),
684 fake_account_from_array([0xfe; 32]),
685 fake_account_from_array([0xfd; 32]),
686 fake_account_from_array([0xfc; 32]),
687 ];
688 for &(order_pda, sell_token) in &expected {
689 accounts.push(fake_account(order_pda));
690 accounts.push(fake_account(sell_token));
691 }
692 let data = ix_data![
695 [SettlementInstruction::BeginSettle.discriminator()],
696 [0x37, 0x13], AUCTION_ID.to_le_bytes(), [ORDER_COUNT as u8], [0u8; ORDER_COUNT],
700 ];
701
702 let parsed = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
703
704 let actual: Vec<(Address, Address)> = parsed
705 .orders
706 .iter()
707 .map(|order| {
708 assert_eq!(order.destinations.len(), 0);
709 (
710 *order.order_pda.address(),
711 *order.sell_token_account.address(),
712 )
713 })
714 .collect();
715 assert_eq!(actual, expected);
716 }
717
718 #[test]
719 fn begin_settle_input_rejects_account_count_mismatch() {
720 let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 1 }>();
725 let data = ix_data![
726 [SettlementInstruction::BeginSettle.discriminator()],
727 [0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x00], ];
732 assert_eq!(
733 BeginSettleInput::parse(&data, &accounts).err(),
734 Some(SettlementError::AccountCountNotMatchingOrderCount.into()),
735 );
736 }
737
738 #[test]
739 fn begin_settle_input_rejects_counts_not_summing_to_destinations() {
740 let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 4 }>();
744 let data = ix_data![
745 [SettlementInstruction::BeginSettle.discriminator()],
746 [0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x01], 0u64.to_le_bytes(),
751 0u64.to_le_bytes(),
752 ];
753 assert_eq!(
754 BeginSettleInput::parse(&data, &accounts).err(),
755 Some(SettlementError::TransferCountMismatch.into()),
756 );
757 }
758
759 #[test]
760 fn begin_settle_input_rejects_missing_order_count() {
761 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
764 let data = ix_data![
765 [SettlementInstruction::BeginSettle.discriminator()],
766 [0, 0], AUCTION_ID.to_le_bytes(), ];
769 assert_eq!(
770 BeginSettleInput::parse(&data, &accounts).err(),
771 Some(ProgramError::InvalidInstructionData),
772 );
773 }
774
775 #[test]
776 fn begin_settle_input_rejects_body_too_short_for_counts() {
777 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
780 let data = ix_data![
781 [SettlementInstruction::BeginSettle.discriminator()],
782 [0, 0], AUCTION_ID.to_le_bytes(), [0x02], [0x00], ];
787 assert_eq!(
788 BeginSettleInput::parse(&data, &accounts).err(),
789 Some(ProgramError::InvalidInstructionData),
790 );
791 }
792
793 #[test]
794 fn begin_settle_input_rejects_partial_amount() {
795 let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
798 let data = ix_data![
799 [SettlementInstruction::BeginSettle.discriminator()],
800 [0, 0], AUCTION_ID.to_le_bytes(), [0x01], [0x00], [0x11, 0x22, 0x33, 0x44], ];
806 assert_eq!(
807 BeginSettleInput::parse(&data, &accounts).err(),
808 Some(ProgramError::InvalidInstructionData),
809 );
810 }
811}