Skip to main content

cow_settlement_interface/instruction/settle/
begin.rs

1//! Off-chain builder and input parsing for the `BeginSettle` instruction.
2
3use 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/// A single transfer made when settling an order: `amount` tokens sent from the
15/// order's sell token account to `destination`.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct Pull {
18    pub destination: Pubkey,
19    pub amount: u64,
20}
21
22/// Builder for a `BeginSettle` instruction settling the orders described by the
23/// parallel lists:
24/// - `order_pdas[i]` is the canonical order PDA (see [`crate::pda::order`])
25/// - `sell_token_accounts[i]` is the order's sell token account,
26/// - `pulls[i]` the list of [`Pull`]s to perform from that order's sell token
27///   account, each sending an amount from the `i`-th order sell token account
28///   to a destination.
29///
30/// The slices are assumed to have the same length but this is not enforced in
31/// the builder.
32///
33/// Wire format (grouped, with `n` orders and `T` total transfers):
34/// `[discriminator=0][finalize_ix_index: u16 LE][auction_id: i64 LE][n: u8]
35/// [transfer_count×n][amount: u64 LE ×T]`.
36/// Required accounts: `[instructions_sysvar (R), state_pda (R), token_program
37/// (R)]` followed, per order, by `[order_pda (W), sell_token_account (W),
38/// destination (W)...]`.
39///
40/// The program requires the order PDAs to be strictly increasing by address.
41/// This builder establishes that ordering for the caller: it sorts the orders by
42/// PDA address, carrying each order's sell token account, transfer count,
43/// amounts, and destination metas before emitting them.
44pub struct BeginSettle<'a> {
45    pub program_id: Pubkey,
46    pub state_pda: Pubkey,
47    pub finalize_ix_index: u16,
48    /// The off-chain auction this settlement executes. Carried in the
49    /// instruction data so the settlement can be tied back to its auction
50    /// off-chain, unused on-chain.
51    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        // Sort the parallel lists together by order PDA address via a shared
70        // permutation, so each order keeps its own sell token account and pulls
71        // (transfer count, amounts, and destination metas).
72        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        // Read-only accounts for instruction introspection, settlement state, and
92        // the SPL token program.
93        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            // Writable account for the order: `BeginSettle` updates its filled
100            // amounts (`amount_withdrawn`/`amount_received`).
101            accounts.push(AccountMeta::new(order_pdas[i], false));
102            // Writable accounts settling the order: its sell token account and the
103            // recipient of each transfer.
104            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
118/// A single settled order, resulted from parsing `BeginSettle`, together with
119/// the funds to pull from its sell token account.
120pub struct SettledOrder<'a, A> {
121    pub order_pda: &'a A,
122    pub sell_token_account: &'a A,
123    /// Destination accounts for this order's transfers.
124    pub destinations: &'a [A],
125    /// Transfer amounts (little-endian `u64`), one per destination.
126    pub amounts: &'a [[u8; 8]],
127}
128
129/// Struct storing accounts, transfer counts, and amounts from parsing the
130/// input of BeginSettle. The parsing step that created this struct guarantees
131/// that there aren't missing elements or that they are assigned incorrectly.
132pub struct SettledOrders<'a, A> {
133    /// Order accounts, laid out per order as
134    /// [order_accounts_1,  order_accounts_2, ...] where
135    /// - each order_accounts is a series of accounts:
136    ///   `order_pda_N, sell_token_account_N, destination_N_1, destination_N_2, ..., destination_N_M`
137    /// - and M is `counts[N]`
138    order_accounts: &'a [A],
139    /// One transfer count per order.
140    counts: &'a [u8],
141    /// Transfer amounts (little-endian `u64`), shared across orders and
142    /// handed out `count` at a time.
143    amounts: &'a [[u8; 8]],
144}
145
146impl<'a, A> SettledOrders<'a, A> {
147    /// Returns an iterator yielding one [`SettledOrder`] per step.
148    #[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        // Cursor over the remaining order accounts; each step splits one order's
155        // `[order_pda, sell_token_account, destinations..count]` off the front.
156        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
185/// Parsed inputs of a `BeginSettle` instruction.
186///
187/// Strictly the raw extracted form. Fields are read from `instruction_data` and
188/// `accounts` but **not validated** against runtime context except confirming
189/// that the discriminator matches the desired input and that the number of
190/// accounts and transfer counts is consistent.
191pub struct BeginSettleInput<'a, A> {
192    pub finalize_ix_index: u16,
193    /// The off-chain auction this settlement executes, read from the instruction
194    /// data. Not validated on-chain: it's carried only so the settlement can be
195    /// tied back to its auction off-chain.
196    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
203/// This implementation defines how instruction bytes and accounts are laid out
204/// in the transaction. It's the source of truth for deciding where the data
205/// is stored.
206impl<'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        // Fixed-width auction id, carried between the counterpart index and the
219        // per-order pull layout so the settlement can be tied to its auction
220        // off-chain.
221        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        // The leading byte is the order count `n`; the counts take `n` bytes and
227        // the remaining bytes are the amounts. `T` (total transfers) is the
228        // number of 8-byte amounts. Too few bytes for the order count or the
229        // counts, or a trailing amount that isn't a whole `u64`, means the data
230        // can't be parsed into the pull layout at all.
231        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        // Each order contributes its order PDA, sell token account, and one
244        // destination per transfer, so the order accounts count is `2n + T`.
245        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        // The transfer counts must sum to `T` so that every destination account
254        // is matched to exactly one amount and the order accounts are consumed
255        // exactly by the iterator.
256        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    /// The fixed accounts every `BeginSettle` carries before its order accounts:
289    /// the instructions sysvar, the settlement state PDA, and the token program.
290    const FIXED_ACCOUNTS: usize = 3;
291
292    /// A placeholder auction id for the tests where its specific value is
293    /// incidental. The wire-layout tests spell out the literal bytes instead.
294    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"),             // counterpart index, little endian
320                hex!("0807060504030201"), // auction id, little endian
321                [0],                      // order count
322            ],
323        );
324        // No orders: the three fixed accounts (sysvar, state PDA, token
325        // program). They are all generic accounts that don't play an active
326        // role in the base instruction (the state PDA CPI signature isn't
327        // relevant here).
328        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        // Two orders supplied in descending PDA order. All the other parameters
339        // are chosen to sort in the opposite order.
340        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"),             // counterpart index, little endian
360                AUCTION_ID.to_le_bytes(), // auction id, little endian
361                [2],                      // order count
362                [0, 0],                   // transfer counts (both zero)
363            ],
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        // The fixed accounts are read-only; the rest are writable and should be sorted
378        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        // Order A has two transfers, order B has one.
408        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"),             // counterpart index, little endian
439                AUCTION_ID.to_le_bytes(), // auction id, little endian
440                [2],                      // order count
441                [2, 1],                   // counts
442                // amounts, little endian
443                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        // The fixed accounts are read-only; the rest are writable
464        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        // The state-PDA and token-program slots are reserved but not surfaced.
480        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],             // finalize index, little-endian
490            hex!("0807060504030201"), // auction id, little-endian
491            [0x00],                   // order count
492        ];
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], // finalize index
514        ];
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], // finalize index
527        ];
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        // The body carries the finalize index but fewer than eight bytes for the
538        // auction id, so parsing can't recover it.
539        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
540        let data = ix_data![
541            [SettlementInstruction::BeginSettle.discriminator()],
542            [0, 0],             // finalize index
543            [0x01, 0x02, 0x03], // a partial (3-byte) auction id
544        ];
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],             // finalize index, little-endian
568            AUCTION_ID.to_le_bytes(), // auction id, little-endian
569            [0x01],                   // order count
570            [0x00],                   // that order's transfer count
571        ];
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],             // finalize index, little-endian
615            AUCTION_ID.to_le_bytes(), // auction id, little-endian
616            [0x01],                   // order count
617            [0x02],                   // transfer count
618            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        // The three fixed accounts (`[0xff..]`, `[0xfe..]`, `[0xfd..]`) differ
651        // from every order/token address above.
652        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        // Grouped data: discriminator, finalize index, auction id, order count,
662        // then all transfer counts (every order has zero transfers).
663        let data = ix_data![
664            [SettlementInstruction::BeginSettle.discriminator()],
665            [0x37, 0x13],             // finalize index, little-endian
666            AUCTION_ID.to_le_bytes(), // auction id, little-endian
667            [ORDER_COUNT as u8],      // order count
668            [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        // The body declares one order with no transfers, which needs exactly two
690        // order accounts (its order PDA and sell token account). Only one order
691        // account is supplied after the fixed accounts, so the number of accounts
692        // doesn't match the `2n + T` the body implies.
693        let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 1 }>();
694        let data = ix_data![
695            [SettlementInstruction::BeginSettle.discriminator()],
696            [0, 0],                   // finalize index
697            AUCTION_ID.to_le_bytes(), // auction id
698            [0x01],                   // order count
699            [0x00],                   // the order's transfer count
700        ];
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        // One order whose two destination accounts (plus its order PDA and sell
710        // token account) make the lengths recover T = 2 transfers, but the
711        // transfer-count byte claims only one.
712        let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 4 }>();
713        let data = ix_data![
714            [SettlementInstruction::BeginSettle.discriminator()],
715            [0, 0],                   // finalize index
716            AUCTION_ID.to_le_bytes(), // auction id
717            [0x01],                   // order count
718            [0x01],                   // count says one, but two amounts/destinations exist
719            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        // The body carries the finalize index and auction id but no order-count
731        // byte, so the pull layout can't even begin to be parsed.
732        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
733        let data = ix_data![
734            [SettlementInstruction::BeginSettle.discriminator()],
735            [0, 0],                   // finalize index
736            AUCTION_ID.to_le_bytes(), // auction id
737        ];
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        // The order count claims two orders, but only one transfer-count byte
747        // follows, so the counts can't be split off.
748        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
749        let data = ix_data![
750            [SettlementInstruction::BeginSettle.discriminator()],
751            [0, 0],                   // finalize index
752            AUCTION_ID.to_le_bytes(), // auction id
753            [0x02],                   // order count: two orders...
754            [0x00],                   // ...but only one transfer-count byte
755        ];
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        // One order with no transfers, but four trailing bytes that don't form a
765        // whole `u64` amount.
766        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
767        let data = ix_data![
768            [SettlementInstruction::BeginSettle.discriminator()],
769            [0, 0],                   // finalize index
770            AUCTION_ID.to_le_bytes(), // auction id
771            [0x01],                   // order count
772            [0x00],                   // transfer count
773            [0x11, 0x22, 0x33, 0x44], // a partial (4-byte) amount
774        ];
775        assert_eq!(
776            BeginSettleInput::parse(&data, &accounts).err(),
777            Some(ProgramError::InvalidInstructionData),
778        );
779    }
780}