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: `[solver (S,R), instructions_sysvar (R), state_pda (R),
37/// token_program (R)]` followed, per order, by `[order_pda (W),
38/// sell_token_account (W), destination (W)...]`.
39///
40/// `solver` must sign, and the solver must be registered in the state pda.
41///
42/// The program requires the order PDAs to be strictly increasing by address.
43/// This builder establishes that ordering for the caller: it sorts the orders by
44/// PDA address, carrying each order's sell token account, transfer count,
45/// amounts, and destination metas before emitting them.
46pub struct BeginSettle<'a> {
47    pub program_id: Pubkey,
48    pub state_pda: Pubkey,
49    pub solver: Pubkey,
50    pub finalize_ix_index: u16,
51    /// The off-chain auction this settlement executes. Carried in the
52    /// instruction data so the settlement can be tied back to its auction
53    /// off-chain, unused on-chain.
54    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        // Sort the parallel lists together by order PDA address via a shared
74        // permutation, so each order keeps its own sell token account and pulls
75        // (transfer count, amounts, and destination metas).
76        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        // The signing solver, followed by read-only accounts for instruction
96        // introspection, settlement state, and the SPL token program.
97        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            // Writable account for the order: `BeginSettle` updates its filled
105            // amounts (`amount_withdrawn`/`amount_received`).
106            accounts.push(AccountMeta::new(order_pdas[i], false));
107            // Writable accounts settling the order: its sell token account and the
108            // recipient of each transfer.
109            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
123/// A single settled order, resulted from parsing `BeginSettle`, together with
124/// the funds to pull from its sell token account.
125pub struct SettledOrder<'a, A> {
126    pub order_pda: &'a A,
127    pub sell_token_account: &'a A,
128    /// Destination accounts for this order's transfers.
129    pub destinations: &'a [A],
130    /// Transfer amounts (little-endian `u64`), one per destination.
131    pub amounts: &'a [[u8; 8]],
132}
133
134/// Struct storing accounts, transfer counts, and amounts from parsing the
135/// input of BeginSettle. The parsing step that created this struct guarantees
136/// that there aren't missing elements or that they are assigned incorrectly.
137pub struct SettledOrders<'a, A> {
138    /// Order accounts, laid out per order as
139    /// [order_accounts_1,  order_accounts_2, ...] where
140    /// - each order_accounts is a series of accounts:
141    ///   `order_pda_N, sell_token_account_N, destination_N_1, destination_N_2, ..., destination_N_M`
142    /// - and M is `counts[N]`
143    order_accounts: &'a [A],
144    /// One transfer count per order.
145    counts: &'a [u8],
146    /// Transfer amounts (little-endian `u64`), shared across orders and
147    /// handed out `count` at a time.
148    amounts: &'a [[u8; 8]],
149}
150
151impl<'a, A> SettledOrders<'a, A> {
152    /// Returns an iterator yielding one [`SettledOrder`] per step.
153    #[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        // Cursor over the remaining order accounts; each step splits one order's
160        // `[order_pda, sell_token_account, destinations..count]` off the front.
161        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
190/// Parsed inputs of a `BeginSettle` instruction.
191///
192/// Strictly the raw extracted form. Fields are read from `instruction_data` and
193/// `accounts` but **not validated** against runtime context except confirming
194/// that the discriminator matches the desired input and that the number of
195/// accounts and transfer counts is consistent.
196pub struct BeginSettleInput<'a, A> {
197    pub finalize_ix_index: u16,
198    /// The off-chain auction this settlement executes, read from the instruction
199    /// data. Not validated on-chain: it's carried only so the settlement can be
200    /// tied back to its auction off-chain.
201    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
209/// This implementation defines how instruction bytes and accounts are laid out
210/// in the transaction. It's the source of truth for deciding where the data
211/// is stored.
212impl<'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        // Fixed-width auction id, carried between the counterpart index and the
225        // per-order pull layout so the settlement can be tied to its auction
226        // off-chain.
227        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        // The leading byte is the order count `n`; the counts take `n` bytes and
233        // the remaining bytes are the amounts. `T` (total transfers) is the
234        // number of 8-byte amounts. Too few bytes for the order count or the
235        // counts, or a trailing amount that isn't a whole `u64`, means the data
236        // can't be parsed into the pull layout at all.
237        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        // Each order contributes its order PDA, sell token account, and one
250        // destination per transfer, so the order accounts count is `2n + T`.
251        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        // The transfer counts must sum to `T` so that every destination account
260        // is matched to exactly one amount and the order accounts are consumed
261        // exactly by the iterator.
262        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    /// The fixed accounts every `BeginSettle` carries before its order accounts:
297    /// the signing solver, the instructions sysvar, the settlement state PDA, and
298    /// the token program.
299    const FIXED_ACCOUNTS: usize = 4;
300
301    /// A placeholder auction id for the tests where its specific value is
302    /// incidental. The wire-layout tests spell out the literal bytes instead.
303    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"),             // counterpart index, little endian
331                hex!("0807060504030201"), // auction id, little endian
332                [0],                      // order count
333            ],
334        );
335        // No orders: the four fixed accounts (solver, sysvar, state PDA, token
336        // program). Only the solver signs; the rest don't play an active role in
337        // the base instruction (the state PDA CPI signature isn't relevant here).
338        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        // Two orders supplied in descending PDA order. All the other parameters
351        // are chosen to sort in the opposite order.
352        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"),             // counterpart index, little endian
373                AUCTION_ID.to_le_bytes(), // auction id, little endian
374                [2],                      // order count
375                [0, 0],                   // transfer counts (both zero)
376            ],
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        // The fixed accounts are read-only; the rest are writable and should be sorted
392        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        // The solver is the sole signer; none of the order accounts signs.
407        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        // Order A has two transfers, order B has one.
425        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"),             // counterpart index, little endian
457                AUCTION_ID.to_le_bytes(), // auction id, little endian
458                [2],                      // order count
459                [2, 1],                   // counts
460                // amounts, little endian
461                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        // The fixed accounts are read-only; the rest are writable
483        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        // The solver is the sole signer; none of the order accounts signs.
493        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],             // finalize index, little-endian
512            hex!("0807060504030201"), // auction id, little-endian
513            [0x00],                   // order count
514        ];
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], // finalize index
538        ];
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], // finalize index
551        ];
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        // The body carries the finalize index but fewer than eight bytes for the
562        // auction id, so parsing can't recover it.
563        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
564        let data = ix_data![
565            [SettlementInstruction::BeginSettle.discriminator()],
566            [0, 0],             // finalize index
567            [0x01, 0x02, 0x03], // a partial (3-byte) auction id
568        ];
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],             // finalize index, little-endian
594            AUCTION_ID.to_le_bytes(), // auction id, little-endian
595            [0x01],                   // order count
596            [0x00],                   // that order's transfer count
597        ];
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],             // finalize index, little-endian
645            AUCTION_ID.to_le_bytes(), // auction id, little-endian
646            [0x01],                   // order count
647            [0x02],                   // transfer count
648            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        // The four fixed accounts (`[0xff..]`, `[0xfe..]`, `[0xfd..]`, `[0xfc..]`)
681        // differ from every order/token address above.
682        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        // Grouped data: discriminator, finalize index, auction id, order count,
693        // then all transfer counts (every order has zero transfers).
694        let data = ix_data![
695            [SettlementInstruction::BeginSettle.discriminator()],
696            [0x37, 0x13],             // finalize index, little-endian
697            AUCTION_ID.to_le_bytes(), // auction id, little-endian
698            [ORDER_COUNT as u8],      // order count
699            [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        // The body declares one order with no transfers, which needs exactly two
721        // order accounts (its order PDA and sell token account). Only one order
722        // account is supplied after the fixed accounts, so the number of accounts
723        // doesn't match the `2n + T` the body implies.
724        let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 1 }>();
725        let data = ix_data![
726            [SettlementInstruction::BeginSettle.discriminator()],
727            [0, 0],                   // finalize index
728            AUCTION_ID.to_le_bytes(), // auction id
729            [0x01],                   // order count
730            [0x00],                   // the order's transfer count
731        ];
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        // One order whose two destination accounts (plus its order PDA and sell
741        // token account) make the lengths recover T = 2 transfers, but the
742        // transfer-count byte claims only one.
743        let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 4 }>();
744        let data = ix_data![
745            [SettlementInstruction::BeginSettle.discriminator()],
746            [0, 0],                   // finalize index
747            AUCTION_ID.to_le_bytes(), // auction id
748            [0x01],                   // order count
749            [0x01],                   // count says one, but two amounts/destinations exist
750            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        // The body carries the finalize index and auction id but no order-count
762        // byte, so the pull layout can't even begin to be parsed.
763        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
764        let data = ix_data![
765            [SettlementInstruction::BeginSettle.discriminator()],
766            [0, 0],                   // finalize index
767            AUCTION_ID.to_le_bytes(), // auction id
768        ];
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        // The order count claims two orders, but only one transfer-count byte
778        // follows, so the counts can't be split off.
779        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
780        let data = ix_data![
781            [SettlementInstruction::BeginSettle.discriminator()],
782            [0, 0],                   // finalize index
783            AUCTION_ID.to_le_bytes(), // auction id
784            [0x02],                   // order count: two orders...
785            [0x00],                   // ...but only one transfer-count byte
786        ];
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        // One order with no transfers, but four trailing bytes that don't form a
796        // whole `u64` amount.
797        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
798        let data = ix_data![
799            [SettlementInstruction::BeginSettle.discriminator()],
800            [0, 0],                   // finalize index
801            AUCTION_ID.to_le_bytes(), // auction id
802            [0x01],                   // order count
803            [0x00],                   // transfer count
804            [0x11, 0x22, 0x33, 0x44], // a partial (4-byte) amount
805        ];
806        assert_eq!(
807            BeginSettleInput::parse(&data, &accounts).err(),
808            Some(ProgramError::InvalidInstructionData),
809        );
810    }
811}