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 mut 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 mut [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. Each order's
148    /// `order_pda` is handed out mutably (so `BeginSettle` can update its filled
149    /// amounts), so the items borrow the iterator and must be processed one at a
150    /// time rather than collected.
151    #[allow(
152        clippy::arithmetic_side_effects,
153        reason = "offsets are bounded by tx limits"
154    )]
155    pub fn iter_mut(&mut self) -> impl Iterator<Item = SettledOrder<'_, A>> + '_ {
156        let (counts, amounts) = (self.counts, self.amounts);
157        // Cursor over the remaining order accounts; each step splits one order's
158        // `[order_pda, sell_token_account, destinations..count]` off the front.
159        let mut rest: &mut [A] = self.order_accounts;
160        let mut i = 0usize;
161        let mut amount_offset = 0usize;
162        std::iter::from_fn(move || {
163            if i >= counts.len() {
164                return None;
165            }
166            let count = usize::from(counts[i]);
167            i += 1;
168
169            let taken = core::mem::take(&mut rest);
170            let (order_pda, tail) = taken.split_first_mut()?;
171            let (sell_token_account, tail) = tail.split_first_mut()?;
172            let (destinations, remainder) = tail.split_at_mut(count);
173            rest = remainder;
174
175            let amount_end = amount_offset + count;
176            let order_amounts = &amounts[amount_offset..amount_end];
177            amount_offset = amount_end;
178
179            Some(SettledOrder {
180                order_pda,
181                sell_token_account,
182                destinations,
183                amounts: order_amounts,
184            })
185        })
186    }
187}
188
189/// Parsed inputs of a `BeginSettle` instruction.
190///
191/// Strictly the raw extracted form. Fields are read from `instruction_data` and
192/// `accounts` but **not validated** against runtime context except confirming
193/// that the discriminator matches the desired input and that the number of
194/// accounts and transfer counts is consistent.
195pub struct BeginSettleInput<'a, A> {
196    pub finalize_ix_index: u16,
197    /// The off-chain auction this settlement executes, read from the instruction
198    /// data. Not validated on-chain: it's carried only so the settlement can be
199    /// tied back to its auction off-chain.
200    pub auction_id: i64,
201    pub instructions_sysvar_account: &'a A,
202    pub state_pda_account: &'a A,
203    pub token_program_account: &'a A,
204    pub orders: SettledOrders<'a, A>,
205}
206
207/// This implementation defines how instruction bytes and accounts are laid out
208/// in the transaction. It's the source of truth for deciding where the data
209/// is stored.
210impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> {
211    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::BeginSettle;
212
213    fn parse_body(instruction_data: &'a [u8], accounts: &'a mut [A]) -> Result<Self, ProgramError> {
214        let (finalize_ix_index, body) = recover_counterpart(instruction_data)?;
215
216        let [instructions_sysvar_account, state_pda_account, token_program_account, order_accounts @ ..] =
217            accounts
218        else {
219            return Err(ProgramError::NotEnoughAccountKeys);
220        };
221
222        // Fixed-width auction id, carried between the counterpart index and the
223        // per-order pull layout so the settlement can be tied to its auction
224        // off-chain.
225        let (auction_id, body) = body
226            .split_first_chunk::<8>()
227            .ok_or(ProgramError::InvalidInstructionData)?;
228        let auction_id = i64::from_le_bytes(*auction_id);
229
230        // The leading byte is the order count `n`; the counts take `n` bytes and
231        // the remaining bytes are the amounts. `T` (total transfers) is the
232        // number of 8-byte amounts. Too few bytes for the order count or the
233        // counts, or a trailing amount that isn't a whole `u64`, means the data
234        // can't be parsed into the pull layout at all.
235        let (&order_count, body) = body
236            .split_first()
237            .ok_or(ProgramError::InvalidInstructionData)?;
238        let order_count = usize::from(order_count);
239        let (counts, amount_bytes) = body
240            .split_at_checked(order_count)
241            .ok_or(ProgramError::InvalidInstructionData)?;
242        let (amounts, []) = amount_bytes.as_chunks::<8>() else {
243            return Err(ProgramError::InvalidInstructionData);
244        };
245        let transfer_count = amounts.len();
246
247        // Each order contributes its order PDA, sell token account, and one
248        // destination per transfer, so the order accounts count is `2n + T`.
249        let expected_accounts = order_count
250            .checked_mul(2)
251            .and_then(|two_n| two_n.checked_add(transfer_count))
252            .ok_or(ProgramError::InvalidInstructionData)?;
253        if order_accounts.len() != expected_accounts {
254            return Err(SettlementError::AccountCountNotMatchingOrderCount.into());
255        }
256
257        // The transfer counts must sum to `T` so that every destination account
258        // is matched to exactly one amount and the order accounts are consumed
259        // exactly by the iterator.
260        let counts_sum: usize = counts.iter().map(|&c| usize::from(c)).sum();
261        if counts_sum != transfer_count {
262            return Err(SettlementError::TransferCountMismatch.into());
263        }
264
265        Ok(Self {
266            finalize_ix_index,
267            auction_id,
268            instructions_sysvar_account,
269            state_pda_account,
270            token_program_account,
271            orders: SettledOrders {
272                order_accounts,
273                counts,
274                amounts,
275            },
276        })
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::instruction::fixtures::{
284        fake_account, fake_account_from_array, fake_sequential_accounts,
285    };
286    use crate::instruction::settle::tests::ix_data;
287    use crate::instruction::tests::assert_readonly_nonsigner;
288    use hex_literal::hex;
289    use solana_account_view::AccountView;
290    use solana_address::Address;
291
292    /// The fixed accounts every `BeginSettle` carries before its order accounts:
293    /// the instructions sysvar, the settlement state PDA, and the token program.
294    const FIXED_ACCOUNTS: usize = 3;
295
296    /// A placeholder auction id for the tests where its specific value is
297    /// incidental. The wire-layout tests spell out the literal bytes instead.
298    const AUCTION_ID: i64 = 0x0102_0304_0506_0708;
299
300    #[test]
301    fn expected_encoding_begin_settle_no_orders() {
302        let program_id = Pubkey::new_unique();
303        let state_pda = Pubkey::new_unique();
304        let Instruction {
305            program_id: ix_program_id,
306            accounts,
307            data,
308        } = BeginSettle {
309            program_id,
310            state_pda,
311            finalize_ix_index: 0x1337,
312            auction_id: 0x0102_0304_0506_0708,
313            order_pdas: &[],
314            sell_token_accounts: &[],
315            pulls: &[],
316        }
317        .into();
318        assert_eq!(ix_program_id, program_id);
319        assert_eq!(
320            data,
321            ix_data![
322                [SettlementInstruction::BeginSettle.discriminator()],
323                hex!("3713"),             // counterpart index, little endian
324                hex!("0807060504030201"), // auction id, little endian
325                [0],                      // order count
326            ],
327        );
328        // No orders: the three fixed accounts (sysvar, state PDA, token
329        // program). They are all generic accounts that don't play an active
330        // role in the base instruction (the state PDA CPI signature isn't
331        // relevant here).
332        assert_eq!(accounts.len(), 3);
333        assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID);
334        assert_readonly_nonsigner(&accounts[1], state_pda);
335        assert_readonly_nonsigner(&accounts[2], SPL_TOKEN_PROGRAM_ID);
336    }
337
338    #[test]
339    fn begin_settle_sorts_orders_by_pda() {
340        let program_id = Pubkey::new_unique();
341        let state_pda = Pubkey::new_unique();
342        // Two orders supplied in descending PDA order. All the other parameters
343        // are chosen to sort in the opposite order.
344        let high_order_pda = Pubkey::new_from_array([0xbb; 32]);
345        let high_sell_token_account = Pubkey::new_from_array([0xa0; 32]);
346        let low_order_pda = Pubkey::new_from_array([0xaa; 32]);
347        let low_sell_token_account = Pubkey::new_from_array([0xb0; 32]);
348        let Instruction { data, accounts, .. } = BeginSettle {
349            program_id,
350            state_pda,
351            finalize_ix_index: 0x1337,
352            auction_id: AUCTION_ID,
353            order_pdas: &[high_order_pda, low_order_pda],
354            sell_token_accounts: &[high_sell_token_account, low_sell_token_account],
355            pulls: &[&[], &[]],
356        }
357        .into();
358
359        assert_eq!(
360            data,
361            ix_data![
362                [SettlementInstruction::BeginSettle.discriminator()],
363                hex!("3713"),             // counterpart index, little endian
364                AUCTION_ID.to_le_bytes(), // auction id, little endian
365                [2],                      // order count
366                [0, 0],                   // transfer counts (both zero)
367            ],
368        );
369
370        let expected: Vec<Pubkey> = vec![
371            INSTRUCTIONS_SYSVAR_ID,
372            state_pda,
373            SPL_TOKEN_PROGRAM_ID,
374            low_order_pda,
375            low_sell_token_account,
376            high_order_pda,
377            high_sell_token_account,
378        ];
379        let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
380        assert_eq!(actual, expected);
381        // The fixed accounts are read-only; the rest are writable and should be sorted
382        let writable: Vec<Pubkey> = accounts
383            .iter()
384            .filter(|account| account.is_writable)
385            .map(|account| account.pubkey)
386            .collect();
387        assert_eq!(
388            writable,
389            vec![
390                low_order_pda,
391                low_sell_token_account,
392                high_order_pda,
393                high_sell_token_account,
394            ],
395        );
396        assert!(accounts.iter().all(|account| !account.is_signer));
397    }
398
399    #[test]
400    fn begin_settle_encodes_grouped_transfers() {
401        let program_id = Pubkey::new_unique();
402        let state_pda = Pubkey::new_unique();
403        let order_a = Pubkey::new_from_array([0x01; 32]);
404        let sell_a = Pubkey::new_from_array([0x02; 32]);
405        let order_b = Pubkey::new_from_array([0x03; 32]);
406        let sell_b = Pubkey::new_from_array([0x04; 32]);
407        let dest_a0 = Pubkey::new_from_array([0x05; 32]);
408        let dest_a1 = Pubkey::new_from_array([0x06; 32]);
409        let dest_b0 = Pubkey::new_from_array([0x07; 32]);
410
411        // Order A has two transfers, order B has one.
412        let Instruction { data, accounts, .. } = BeginSettle {
413            program_id,
414            state_pda,
415            finalize_ix_index: 0x1337,
416            auction_id: AUCTION_ID,
417            order_pdas: &[order_a, order_b],
418            sell_token_accounts: &[sell_a, sell_b],
419            pulls: &[
420                &[
421                    Pull {
422                        destination: dest_a0,
423                        amount: 0x0102,
424                    },
425                    Pull {
426                        destination: dest_a1,
427                        amount: 0x0304,
428                    },
429                ],
430                &[Pull {
431                    destination: dest_b0,
432                    amount: 0x0506,
433                }],
434            ],
435        }
436        .into();
437
438        assert_eq!(
439            data,
440            ix_data![
441                [SettlementInstruction::BeginSettle.discriminator()],
442                hex!("3713"),             // counterpart index, little endian
443                AUCTION_ID.to_le_bytes(), // auction id, little endian
444                [2],                      // order count
445                [2, 1],                   // counts
446                // amounts, little endian
447                hex!("0201000000000000"),
448                hex!("0403000000000000"),
449                hex!("0605000000000000"),
450            ],
451        );
452
453        let expected: Vec<Pubkey> = vec![
454            INSTRUCTIONS_SYSVAR_ID,
455            state_pda,
456            SPL_TOKEN_PROGRAM_ID,
457            order_a,
458            sell_a,
459            dest_a0,
460            dest_a1,
461            order_b,
462            sell_b,
463            dest_b0,
464        ];
465        let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
466        assert_eq!(actual, expected);
467        // The fixed accounts are read-only; the rest are writable
468        let writable: Vec<Pubkey> = accounts
469            .iter()
470            .filter(|account| account.is_writable)
471            .map(|account| account.pubkey)
472            .collect();
473        assert_eq!(
474            writable,
475            vec![order_a, sell_a, dest_a0, dest_a1, order_b, sell_b, dest_b0],
476        );
477        assert!(accounts.iter().all(|account| !account.is_signer));
478    }
479
480    #[test]
481    fn begin_settle_input_parses_valid_input() {
482        let sysvar = Address::new_from_array([0x42u8; 32]);
483        // The state-PDA and token-program slots are reserved but not surfaced.
484        let state = Address::new_from_array([0x43u8; 32]);
485        let token_program = Address::new_from_array([0x44u8; 32]);
486        let mut accounts = [
487            fake_account(sysvar),
488            fake_account(state),
489            fake_account(token_program),
490        ];
491        let data = ix_data![
492            [SettlementInstruction::BeginSettle.discriminator()],
493            [0x37, 0x13],             // finalize index, little-endian
494            hex!("0807060504030201"), // auction id, little-endian
495            [0x00],                   // order count
496        ];
497        let BeginSettleInput {
498            finalize_ix_index,
499            auction_id,
500            instructions_sysvar_account,
501            mut orders,
502            token_program_account,
503            state_pda_account,
504        } = BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
505        assert_eq!(finalize_ix_index, 0x1337);
506        assert_eq!(auction_id, 0x0102_0304_0506_0708);
507        assert_eq!(instructions_sysvar_account.address(), &sysvar);
508        assert_eq!(orders.iter_mut().count(), 0);
509        assert_eq!(token_program_account.address(), &token_program);
510        assert_eq!(state_pda_account.address(), &state);
511    }
512
513    #[test]
514    fn begin_settle_input_rejects_different_discriminator() {
515        let data = ix_data![
516            [SettlementInstruction::FinalizeSettle.discriminator()],
517            [0, 0], // finalize index
518        ];
519        let mut accounts: [AccountView; 0] = [];
520        assert_eq!(
521            BeginSettleInput::parse(&data, &mut accounts).err(),
522            Some(ProgramError::InvalidInstructionData),
523        );
524    }
525
526    #[test]
527    fn begin_settle_input_rejects_empty_accounts() {
528        let data = ix_data![
529            [SettlementInstruction::BeginSettle.discriminator()],
530            [0, 0], // finalize index
531        ];
532        let mut accounts: [AccountView; 0] = [];
533        assert_eq!(
534            BeginSettleInput::parse(&data, &mut accounts).err(),
535            Some(ProgramError::NotEnoughAccountKeys),
536        );
537    }
538
539    #[test]
540    fn begin_settle_input_rejects_missing_auction_id() {
541        // The body carries the finalize index but fewer than eight bytes for the
542        // auction id, so parsing can't recover it.
543        let mut accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
544        let data = ix_data![
545            [SettlementInstruction::BeginSettle.discriminator()],
546            [0, 0],             // finalize index
547            [0x01, 0x02, 0x03], // a partial (3-byte) auction id
548        ];
549        assert_eq!(
550            BeginSettleInput::parse(&data, &mut accounts).err(),
551            Some(ProgramError::InvalidInstructionData),
552        );
553    }
554
555    #[test]
556    fn begin_settle_input_pairs_orders_with_their_accounts() {
557        let sysvar = Address::new_from_array([1u8; 32]);
558        let state = Address::new_from_array([0xa1u8; 32]);
559        let token_program = Address::new_from_array([0xa2u8; 32]);
560        let order_pda = Address::new_from_array([2u8; 32]);
561        let sell_token = Address::new_from_array([3u8; 32]);
562        let mut accounts = [
563            fake_account(sysvar),
564            fake_account(state),
565            fake_account(token_program),
566            fake_account(order_pda),
567            fake_account(sell_token),
568        ];
569        let data = ix_data![
570            [SettlementInstruction::BeginSettle.discriminator()],
571            [0x37, 0x13],             // finalize index, little-endian
572            AUCTION_ID.to_le_bytes(), // auction id, little-endian
573            [0x01],                   // order count
574            [0x00],                   // that order's transfer count
575        ];
576        let BeginSettleInput {
577            finalize_ix_index,
578            auction_id,
579            instructions_sysvar_account,
580            mut orders,
581            state_pda_account,
582            token_program_account,
583        } = BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
584        assert_eq!(finalize_ix_index, 0x1337);
585        assert_eq!(auction_id, AUCTION_ID);
586        assert_eq!(instructions_sysvar_account.address(), &sysvar);
587        assert_eq!(token_program_account.address(), &token_program);
588        assert_eq!(state_pda_account.address(), &state);
589
590        let mut orders = orders.iter_mut();
591        let order = orders.next().expect("one settled order");
592        assert_eq!(order.order_pda.address(), &order_pda);
593        assert_eq!(order.sell_token_account.address(), &sell_token);
594        assert_eq!(order.destinations.len(), 0);
595        assert!(orders.next().is_none());
596    }
597
598    #[test]
599    fn begin_settle_input_parses_transfers() {
600        let sysvar = Address::new_from_array([1u8; 32]);
601        let state = Address::new_from_array([0xa1u8; 32]);
602        let token_program = Address::new_from_array([0xa2u8; 32]);
603        let order_pda = Address::new_from_array([2u8; 32]);
604        let sell_token = Address::new_from_array([3u8; 32]);
605        let dest0 = Address::new_from_array([4u8; 32]);
606        let dest1 = Address::new_from_array([5u8; 32]);
607        let mut accounts = [
608            fake_account(sysvar),
609            fake_account(state),
610            fake_account(token_program),
611            fake_account(order_pda),
612            fake_account(sell_token),
613            fake_account(dest0),
614            fake_account(dest1),
615        ];
616        let data = ix_data![
617            [SettlementInstruction::BeginSettle.discriminator()],
618            [0x37, 0x13],             // finalize index, little-endian
619            AUCTION_ID.to_le_bytes(), // auction id, little-endian
620            [0x01],                   // order count
621            [0x02],                   // transfer count
622            0x1122u64.to_le_bytes(),
623            0x3344u64.to_le_bytes(),
624        ];
625
626        let BeginSettleInput { mut orders, .. } =
627            BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
628
629        let mut orders = orders.iter_mut();
630        let order = orders.next().expect("one settled order");
631        assert_eq!(order.order_pda.address(), &order_pda);
632        assert_eq!(order.sell_token_account.address(), &sell_token);
633        let transfers: Vec<(&Address, u64)> = order
634            .destinations
635            .iter()
636            .zip(order.amounts)
637            .map(|(destination, amount)| (destination.address(), u64::from_le_bytes(*amount)))
638            .collect();
639        assert_eq!(transfers, vec![(&dest0, 0x1122), (&dest1, 0x3344)]);
640        assert!(orders.next().is_none());
641    }
642
643    #[test]
644    fn begin_settle_input_pairs_every_order_with_its_sell_token_account() {
645        const ORDER_COUNT: usize = 16;
646
647        let mut expected: Vec<(Address, Address)> = Vec::new();
648        for i in 0..ORDER_COUNT {
649            let order_pda = Address::new_from_array([i as u8; 32]);
650            let sell_token = Address::new_from_array([(i + ORDER_COUNT) as u8; 32]);
651            expected.push((order_pda, sell_token));
652        }
653
654        // The three fixed accounts (`[0xff..]`, `[0xfe..]`, `[0xfd..]`) differ
655        // from every order/token address above.
656        let mut accounts = vec![
657            fake_account_from_array([0xff; 32]),
658            fake_account_from_array([0xfe; 32]),
659            fake_account_from_array([0xfd; 32]),
660        ];
661        for &(order_pda, sell_token) in &expected {
662            accounts.push(fake_account(order_pda));
663            accounts.push(fake_account(sell_token));
664        }
665        // Grouped data: discriminator, finalize index, auction id, order count,
666        // then all transfer counts (every order has zero transfers).
667        let data = ix_data![
668            [SettlementInstruction::BeginSettle.discriminator()],
669            [0x37, 0x13],             // finalize index, little-endian
670            AUCTION_ID.to_le_bytes(), // auction id, little-endian
671            [ORDER_COUNT as u8],      // order count
672            [0u8; ORDER_COUNT],
673        ];
674
675        let mut parsed =
676            BeginSettleInput::parse(&data, &mut accounts).expect("parse should succeed");
677
678        let actual: Vec<(Address, Address)> = parsed
679            .orders
680            .iter_mut()
681            .map(|order| {
682                assert_eq!(order.destinations.len(), 0);
683                (
684                    *order.order_pda.address(),
685                    *order.sell_token_account.address(),
686                )
687            })
688            .collect();
689        assert_eq!(actual, expected);
690    }
691
692    #[test]
693    fn begin_settle_input_rejects_account_count_mismatch() {
694        // The body declares one order with no transfers, which needs exactly two
695        // order accounts (its order PDA and sell token account). Only one order
696        // account is supplied after the fixed accounts, so the number of accounts
697        // doesn't match the `2n + T` the body implies.
698        let mut accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 1 }>();
699        let data = ix_data![
700            [SettlementInstruction::BeginSettle.discriminator()],
701            [0, 0],                   // finalize index
702            AUCTION_ID.to_le_bytes(), // auction id
703            [0x01],                   // order count
704            [0x00],                   // the order's transfer count
705        ];
706        assert_eq!(
707            BeginSettleInput::parse(&data, &mut accounts).err(),
708            Some(SettlementError::AccountCountNotMatchingOrderCount.into()),
709        );
710    }
711
712    #[test]
713    fn begin_settle_input_rejects_counts_not_summing_to_destinations() {
714        // One order whose two destination accounts (plus its order PDA and sell
715        // token account) make the lengths recover T = 2 transfers, but the
716        // transfer-count byte claims only one.
717        let mut accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 4 }>();
718        let data = ix_data![
719            [SettlementInstruction::BeginSettle.discriminator()],
720            [0, 0],                   // finalize index
721            AUCTION_ID.to_le_bytes(), // auction id
722            [0x01],                   // order count
723            [0x01],                   // count says one, but two amounts/destinations exist
724            0u64.to_le_bytes(),
725            0u64.to_le_bytes(),
726        ];
727        assert_eq!(
728            BeginSettleInput::parse(&data, &mut accounts).err(),
729            Some(SettlementError::TransferCountMismatch.into()),
730        );
731    }
732
733    #[test]
734    fn begin_settle_input_rejects_missing_order_count() {
735        // The body carries the finalize index and auction id but no order-count
736        // byte, so the pull layout can't even begin to be parsed.
737        let mut accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
738        let data = ix_data![
739            [SettlementInstruction::BeginSettle.discriminator()],
740            [0, 0],                   // finalize index
741            AUCTION_ID.to_le_bytes(), // auction id
742        ];
743        assert_eq!(
744            BeginSettleInput::parse(&data, &mut accounts).err(),
745            Some(ProgramError::InvalidInstructionData),
746        );
747    }
748
749    #[test]
750    fn begin_settle_input_rejects_body_too_short_for_counts() {
751        // The order count claims two orders, but only one transfer-count byte
752        // follows, so the counts can't be split off.
753        let mut accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
754        let data = ix_data![
755            [SettlementInstruction::BeginSettle.discriminator()],
756            [0, 0],                   // finalize index
757            AUCTION_ID.to_le_bytes(), // auction id
758            [0x02],                   // order count: two orders...
759            [0x00],                   // ...but only one transfer-count byte
760        ];
761        assert_eq!(
762            BeginSettleInput::parse(&data, &mut accounts).err(),
763            Some(ProgramError::InvalidInstructionData),
764        );
765    }
766
767    #[test]
768    fn begin_settle_input_rejects_partial_amount() {
769        // One order with no transfers, but four trailing bytes that don't form a
770        // whole `u64` amount.
771        let mut accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
772        let data = ix_data![
773            [SettlementInstruction::BeginSettle.discriminator()],
774            [0, 0],                   // finalize index
775            AUCTION_ID.to_le_bytes(), // auction id
776            [0x01],                   // order count
777            [0x00],                   // transfer count
778            [0x11, 0x22, 0x33, 0x44], // a partial (4-byte) amount
779        ];
780        assert_eq!(
781            BeginSettleInput::parse(&data, &mut accounts).err(),
782            Some(ProgramError::InvalidInstructionData),
783        );
784    }
785}