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, TokenProgram, INSTRUCTIONS_SYSVAR_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/// spl_token_program (R), token_2022_program (R)]` followed, per order, by
38/// `[order_pda (W), sell_token_account (W), destination (W)...]`. The token
39/// program accounts are there to allow CPI calls against the corresponding token
40/// program, and are otherwise not parsed or validated, so it is possible to replace
41/// these accounts with the system program (or any other program) if they are unused.
42///
43/// `solver` must sign, and the solver must be registered in the state pda.
44///
45/// The program requires the order PDAs to be strictly increasing by address.
46/// This builder establishes that ordering for the caller: it sorts the orders by
47/// PDA address, carrying each order's sell token account, transfer count,
48/// amounts, and destination metas before emitting them.
49pub struct BeginSettle<'a> {
50    pub program_id: Pubkey,
51    pub state_pda: Pubkey,
52    pub solver: Pubkey,
53    pub finalize_ix_index: u16,
54    /// The off-chain auction this settlement executes. Carried in the
55    /// instruction data so the settlement can be tied back to its auction
56    /// off-chain, unused on-chain.
57    pub auction_id: i64,
58    /// The only token program this settlement's transfers are issued against,
59    /// or `None` to name every supported one; see [`TokenProgram::ALL`].
60    pub only_token_program: Option<TokenProgram>,
61    pub order_pdas: &'a [Pubkey],
62    pub sell_token_accounts: &'a [Pubkey],
63    pub pulls: &'a [&'a [Pull]],
64}
65
66impl From<BeginSettle<'_>> for Instruction {
67    fn from(builder: BeginSettle<'_>) -> Self {
68        let BeginSettle {
69            program_id,
70            state_pda,
71            solver,
72            finalize_ix_index,
73            auction_id,
74            only_token_program,
75            order_pdas,
76            sell_token_accounts,
77            pulls,
78        } = builder;
79
80        // Sort the parallel lists together by order PDA address via a shared
81        // permutation, so each order keeps its own sell token account and pulls
82        // (transfer count, amounts, and destination metas).
83        let mut order: Vec<usize> = (0..order_pdas.len()).collect();
84        order.sort_by_key(|&i| order_pdas[i]);
85
86        let counts: Vec<u8> = order.iter().map(|&i| pulls[i].len() as u8).collect();
87        let amounts: Vec<u8> = order
88            .iter()
89            .flat_map(|&i| pulls[i].iter())
90            .flat_map(|pull| pull.amount.to_le_bytes())
91            .collect();
92        let data = [
93            &[SettlementInstruction::BeginSettle.discriminator()][..],
94            &finalize_ix_index.to_le_bytes()[..],
95            &auction_id.to_le_bytes()[..],
96            &[order_pdas.len() as u8][..],
97            &counts[..],
98            &amounts[..],
99        ]
100        .concat();
101
102        // The signing solver, followed by read-only accounts for instruction
103        // introspection, settlement state, and one slot per supported token
104        // program.
105        let mut accounts = vec![
106            AccountMeta::new_readonly(solver, true),
107            AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
108            AccountMeta::new_readonly(state_pda, false),
109        ];
110        // One account per supported token program. If `only_token_program`,
111        // replace the other program in the instruction with an account that's
112        // already present (and so doesn't take extra space in the tx).
113        accounts.extend(TokenProgram::ALL.map(|program| {
114            let address = if only_token_program.is_none_or(|only| only == program) {
115                program.address()
116            } else {
117                INSTRUCTIONS_SYSVAR_ID
118            };
119            AccountMeta::new_readonly(address, false)
120        }));
121        for &i in &order {
122            // Writable account for the order: `BeginSettle` updates its filled
123            // amounts (`amount_withdrawn`/`amount_received`).
124            accounts.push(AccountMeta::new(order_pdas[i], false));
125            // Writable accounts settling the order: its sell token account and the
126            // recipient of each transfer.
127            accounts.push(AccountMeta::new(sell_token_accounts[i], false));
128            for pull in pulls[i] {
129                accounts.push(AccountMeta::new(pull.destination, false));
130            }
131        }
132
133        Instruction {
134            program_id,
135            accounts,
136            data,
137        }
138    }
139}
140
141/// A single settled order, resulted from parsing `BeginSettle`, together with
142/// the funds to pull from its sell token account.
143pub struct SettledOrder<'a, A> {
144    pub order_pda: &'a A,
145    pub sell_token_account: &'a A,
146    /// Destination accounts for this order's transfers.
147    pub destinations: &'a [A],
148    /// Transfer amounts (little-endian `u64`), one per destination.
149    pub amounts: &'a [[u8; 8]],
150}
151
152/// Struct storing accounts, transfer counts, and amounts from parsing the
153/// input of BeginSettle. The parsing step that created this struct guarantees
154/// that there aren't missing elements or that they are assigned incorrectly.
155pub struct SettledOrders<'a, A> {
156    /// Order accounts, laid out per order as
157    /// [order_accounts_1,  order_accounts_2, ...] where
158    /// - each order_accounts is a series of accounts:
159    ///   `order_pda_N, sell_token_account_N, destination_N_1, destination_N_2, ..., destination_N_M`
160    /// - and M is `counts[N]`
161    order_accounts: &'a [A],
162    /// One transfer count per order.
163    counts: &'a [u8],
164    /// Transfer amounts (little-endian `u64`), shared across orders and
165    /// handed out `count` at a time.
166    amounts: &'a [[u8; 8]],
167}
168
169impl<'a, A> SettledOrders<'a, A> {
170    /// Returns an iterator yielding one [`SettledOrder`] per step.
171    #[allow(
172        clippy::arithmetic_side_effects,
173        reason = "offsets are bounded by tx limits"
174    )]
175    pub fn iter(&self) -> impl Iterator<Item = SettledOrder<'a, A>> + '_ {
176        let (counts, amounts) = (self.counts, self.amounts);
177        // Cursor over the remaining order accounts; each step splits one order's
178        // `[order_pda, sell_token_account, destinations..count]` off the front.
179        let mut rest: &'a [A] = self.order_accounts;
180        let mut i = 0usize;
181        let mut amount_offset = 0usize;
182        std::iter::from_fn(move || {
183            if i >= counts.len() {
184                return None;
185            }
186            let count = usize::from(counts[i]);
187            i += 1;
188
189            let (order_pda, tail) = rest.split_first()?;
190            let (sell_token_account, tail) = tail.split_first()?;
191            let (destinations, remainder) = tail.split_at(count);
192            rest = remainder;
193
194            let amount_end = amount_offset + count;
195            let order_amounts = &amounts[amount_offset..amount_end];
196            amount_offset = amount_end;
197
198            Some(SettledOrder {
199                order_pda,
200                sell_token_account,
201                destinations,
202                amounts: order_amounts,
203            })
204        })
205    }
206}
207
208/// Parsed inputs of a `BeginSettle` instruction.
209///
210/// Strictly the raw extracted form. Fields are read from `instruction_data` and
211/// `accounts` but **not validated** against runtime context except confirming
212/// that the discriminator matches the desired input and that the number of
213/// accounts and transfer counts is consistent.
214pub struct BeginSettleInput<'a, A> {
215    pub finalize_ix_index: u16,
216    /// The off-chain auction this settlement executes, read from the instruction
217    /// data. Not validated on-chain: it's carried only so the settlement can be
218    /// tied back to its auction off-chain.
219    pub auction_id: i64,
220    pub solver_account: &'a A,
221    pub instructions_sysvar_account: &'a A,
222    pub state_pda_account: &'a A,
223    pub orders: SettledOrders<'a, A>,
224}
225
226/// This implementation defines how instruction bytes and accounts are laid out
227/// in the transaction. It's the source of truth for deciding where the data
228/// is stored.
229impl<'a, A> InstructionInputParsing<'a, A> for BeginSettleInput<'a, A> {
230    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::BeginSettle;
231
232    fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
233        let (finalize_ix_index, body) = recover_counterpart(instruction_data)?;
234
235        // The two token-program slots are skipped rather than read since they are only
236        // used for program invocation.
237        let [solver_account, instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, order_accounts @ ..] =
238            accounts
239        else {
240            return Err(ProgramError::NotEnoughAccountKeys);
241        };
242
243        // Fixed-width auction id, carried between the counterpart index and the
244        // per-order pull layout so the settlement can be tied to its auction
245        // off-chain.
246        let (auction_id, body) = body
247            .split_first_chunk::<8>()
248            .ok_or(ProgramError::InvalidInstructionData)?;
249        let auction_id = i64::from_le_bytes(*auction_id);
250
251        // The leading byte is the order count `n`; the counts take `n` bytes and
252        // the remaining bytes are the amounts. `T` (total transfers) is the
253        // number of 8-byte amounts. Too few bytes for the order count or the
254        // counts, or a trailing amount that isn't a whole `u64`, means the data
255        // can't be parsed into the pull layout at all.
256        let (&order_count, body) = body
257            .split_first()
258            .ok_or(ProgramError::InvalidInstructionData)?;
259        let order_count = usize::from(order_count);
260        let (counts, amount_bytes) = body
261            .split_at_checked(order_count)
262            .ok_or(ProgramError::InvalidInstructionData)?;
263        let (amounts, []) = amount_bytes.as_chunks::<8>() else {
264            return Err(ProgramError::InvalidInstructionData);
265        };
266        let transfer_count = amounts.len();
267
268        // Each order contributes its order PDA, sell token account, and one
269        // destination per transfer, so the order accounts count is `2n + T`.
270        let expected_accounts = order_count
271            .checked_mul(2)
272            .and_then(|two_n| two_n.checked_add(transfer_count))
273            .ok_or(ProgramError::InvalidInstructionData)?;
274        if order_accounts.len() != expected_accounts {
275            return Err(SettlementError::AccountCountNotMatchingOrderCount.into());
276        }
277
278        // The transfer counts must sum to `T` so that every destination account
279        // is matched to exactly one amount and the order accounts are consumed
280        // exactly by the iterator.
281        let counts_sum: usize = counts.iter().map(|&c| usize::from(c)).sum();
282        if counts_sum != transfer_count {
283            return Err(SettlementError::TransferCountMismatch.into());
284        }
285
286        Ok(Self {
287            finalize_ix_index,
288            auction_id,
289            instructions_sysvar_account,
290            state_pda_account,
291            solver_account,
292            orders: SettledOrders {
293                order_accounts,
294                counts,
295                amounts,
296            },
297        })
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::fixtures::pubkey_from_seed;
305    use crate::instruction::fixtures::{
306        fake_account, fake_account_from_array, fake_sequential_accounts,
307    };
308    use crate::instruction::settle::tests::ix_data;
309    use crate::instruction::tests::{assert_readonly_nonsigner, assert_readonly_signer};
310    use crate::token_program::TokenProgram;
311    use hex_literal::hex;
312    use solana_account_view::AccountView;
313    use solana_address::Address;
314
315    /// The fixed accounts every `BeginSettle` carries before its order accounts:
316    /// the signing solver, the instructions sysvar, the settlement state PDA, and
317    /// one slot per supported token program.
318    const FIXED_ACCOUNTS: usize = 5;
319
320    /// A placeholder auction id for the tests where its specific value is
321    /// incidental. The wire-layout tests spell out the literal bytes instead.
322    const AUCTION_ID: i64 = 0x0102_0304_0506_0708;
323
324    #[test]
325    fn expected_encoding_begin_settle_no_orders() {
326        let program_id = pubkey_from_seed("program id");
327        let state_pda = pubkey_from_seed("state pda");
328        let solver = pubkey_from_seed("solver");
329        let Instruction {
330            program_id: ix_program_id,
331            accounts,
332            data,
333        } = BeginSettle {
334            program_id,
335            state_pda,
336            solver,
337            finalize_ix_index: 0x1337,
338            auction_id: 0x0102_0304_0506_0708,
339            only_token_program: None,
340            order_pdas: &[],
341            sell_token_accounts: &[],
342            pulls: &[],
343        }
344        .into();
345        assert_eq!(ix_program_id, program_id);
346        assert_eq!(
347            data,
348            ix_data![
349                [SettlementInstruction::BeginSettle.discriminator()],
350                hex!("3713"),             // counterpart index, little endian
351                hex!("0807060504030201"), // auction id, little endian
352                [0],                      // order count
353            ],
354        );
355        // No orders: the fixed accounts (solver, sysvar, state PDA, and a slot
356        // per token program). Only the solver signs; the rest don't play an
357        // active role in the base instruction (the state PDA CPI signature isn't
358        // relevant here). This settlement carries only the legacy program, so
359        // Token-2022's slot holds the placeholder.
360        assert_eq!(accounts.len(), FIXED_ACCOUNTS);
361        assert_readonly_signer(&accounts[0], solver);
362        assert_readonly_nonsigner(&accounts[1], INSTRUCTIONS_SYSVAR_ID);
363        assert_readonly_nonsigner(&accounts[2], state_pda);
364        assert_readonly_nonsigner(&accounts[3], TokenProgram::SplToken.address());
365        assert_readonly_nonsigner(&accounts[4], TokenProgram::Token2022.address());
366    }
367
368    /// The token-program slots are the addresses the settlement's
369    /// `only_token_program` names, in [`TokenProgram::ALL`] order, so a
370    /// settlement can name both programs — or leave either one out.
371    #[test]
372    fn begin_settle_carries_the_token_program_slots_it_is_given() {
373        for (only_token_program, expected) in [
374            (
375                None,
376                [
377                    TokenProgram::SplToken.address(),
378                    TokenProgram::Token2022.address(),
379                ],
380            ),
381            (
382                Some(TokenProgram::SplToken),
383                [TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID],
384            ),
385            (
386                Some(TokenProgram::Token2022),
387                [INSTRUCTIONS_SYSVAR_ID, TokenProgram::Token2022.address()],
388            ),
389        ] {
390            let Instruction { accounts, .. } = Instruction::from(BeginSettle {
391                program_id: Pubkey::new_unique(),
392                state_pda: Pubkey::new_unique(),
393                solver: Pubkey::new_unique(),
394                finalize_ix_index: 0,
395                auction_id: 0,
396                only_token_program,
397                order_pdas: &[],
398                sell_token_accounts: &[],
399                pulls: &[],
400            });
401            let slots: Vec<Pubkey> = accounts[3..].iter().map(|meta| meta.pubkey).collect();
402            assert_eq!(
403                slots, expected,
404                "{only_token_program:?} should name just the programs it settles against",
405            );
406        }
407    }
408
409    #[test]
410    fn begin_settle_sorts_orders_by_pda() {
411        let program_id = pubkey_from_seed("program id");
412        let state_pda = pubkey_from_seed("state pda");
413        let solver = pubkey_from_seed("solver");
414        // Two orders supplied in descending PDA order. All the other parameters
415        // are chosen to sort in the opposite order.
416        let high_order_pda = Pubkey::new_from_array([0xbb; 32]);
417        let high_sell_token_account = Pubkey::new_from_array([0xa0; 32]);
418        let low_order_pda = Pubkey::new_from_array([0xaa; 32]);
419        let low_sell_token_account = Pubkey::new_from_array([0xb0; 32]);
420        let Instruction { data, accounts, .. } = BeginSettle {
421            program_id,
422            state_pda,
423            solver,
424            finalize_ix_index: 0x1337,
425            auction_id: AUCTION_ID,
426            only_token_program: None,
427            order_pdas: &[high_order_pda, low_order_pda],
428            sell_token_accounts: &[high_sell_token_account, low_sell_token_account],
429            pulls: &[&[], &[]],
430        }
431        .into();
432
433        assert_eq!(
434            data,
435            ix_data![
436                [SettlementInstruction::BeginSettle.discriminator()],
437                hex!("3713"),             // counterpart index, little endian
438                AUCTION_ID.to_le_bytes(), // auction id, little endian
439                [2],                      // order count
440                [0, 0],                   // transfer counts (both zero)
441            ],
442        );
443
444        let expected: Vec<Pubkey> = vec![
445            solver,
446            INSTRUCTIONS_SYSVAR_ID,
447            state_pda,
448            TokenProgram::SplToken.address(),
449            TokenProgram::Token2022.address(),
450            low_order_pda,
451            low_sell_token_account,
452            high_order_pda,
453            high_sell_token_account,
454        ];
455        let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
456        assert_eq!(actual, expected);
457        // The fixed accounts are read-only; the rest are writable and should be sorted
458        let writable: Vec<Pubkey> = accounts
459            .iter()
460            .filter(|account| account.is_writable)
461            .map(|account| account.pubkey)
462            .collect();
463        assert_eq!(
464            writable,
465            vec![
466                low_order_pda,
467                low_sell_token_account,
468                high_order_pda,
469                high_sell_token_account,
470            ],
471        );
472        // The solver is the sole signer; none of the order accounts signs.
473        let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS);
474        assert!(order_accounts.iter().all(|account| !account.is_signer));
475    }
476
477    #[test]
478    fn begin_settle_encodes_grouped_transfers() {
479        let program_id = pubkey_from_seed("program id");
480        let state_pda = pubkey_from_seed("state pda");
481        let solver = pubkey_from_seed("solver");
482        let order_a = Pubkey::new_from_array([0x01; 32]);
483        let sell_a = Pubkey::new_from_array([0x02; 32]);
484        let order_b = Pubkey::new_from_array([0x03; 32]);
485        let sell_b = Pubkey::new_from_array([0x04; 32]);
486        let dest_a0 = Pubkey::new_from_array([0x05; 32]);
487        let dest_a1 = Pubkey::new_from_array([0x06; 32]);
488        let dest_b0 = Pubkey::new_from_array([0x07; 32]);
489
490        // Order A has two transfers, order B has one.
491        let Instruction { data, accounts, .. } = BeginSettle {
492            program_id,
493            state_pda,
494            solver,
495            finalize_ix_index: 0x1337,
496            auction_id: AUCTION_ID,
497            only_token_program: None,
498            order_pdas: &[order_a, order_b],
499            sell_token_accounts: &[sell_a, sell_b],
500            pulls: &[
501                &[
502                    Pull {
503                        destination: dest_a0,
504                        amount: 0x0102,
505                    },
506                    Pull {
507                        destination: dest_a1,
508                        amount: 0x0304,
509                    },
510                ],
511                &[Pull {
512                    destination: dest_b0,
513                    amount: 0x0506,
514                }],
515            ],
516        }
517        .into();
518
519        assert_eq!(
520            data,
521            ix_data![
522                [SettlementInstruction::BeginSettle.discriminator()],
523                hex!("3713"),             // counterpart index, little endian
524                AUCTION_ID.to_le_bytes(), // auction id, little endian
525                [2],                      // order count
526                [2, 1],                   // counts
527                // amounts, little endian
528                hex!("0201000000000000"),
529                hex!("0403000000000000"),
530                hex!("0605000000000000"),
531            ],
532        );
533
534        let expected: Vec<Pubkey> = vec![
535            solver,
536            INSTRUCTIONS_SYSVAR_ID,
537            state_pda,
538            TokenProgram::SplToken.address(),
539            TokenProgram::Token2022.address(),
540            order_a,
541            sell_a,
542            dest_a0,
543            dest_a1,
544            order_b,
545            sell_b,
546            dest_b0,
547        ];
548        let actual: Vec<Pubkey> = accounts.iter().map(|account| account.pubkey).collect();
549        assert_eq!(actual, expected);
550        // The fixed accounts are read-only; the rest are writable
551        let writable: Vec<Pubkey> = accounts
552            .iter()
553            .filter(|account| account.is_writable)
554            .map(|account| account.pubkey)
555            .collect();
556        assert_eq!(
557            writable,
558            vec![order_a, sell_a, dest_a0, dest_a1, order_b, sell_b, dest_b0],
559        );
560        // The solver is the sole signer; none of the order accounts signs.
561        let (_fixed, order_accounts) = accounts.split_at(FIXED_ACCOUNTS);
562        assert!(order_accounts.iter().all(|account| !account.is_signer));
563    }
564
565    #[test]
566    fn begin_settle_input_parses_valid_input() {
567        let sysvar = pubkey_from_seed("sysvar");
568        let state = pubkey_from_seed("state pda");
569        let spl_token_program = pubkey_from_seed("spl token program");
570        let token_2022_program = pubkey_from_seed("token 2022 program");
571        let solver = pubkey_from_seed("solver");
572        let accounts = [
573            fake_account(solver),
574            fake_account(sysvar),
575            fake_account(state),
576            fake_account(spl_token_program),
577            fake_account(token_2022_program),
578        ];
579        let data = ix_data![
580            [SettlementInstruction::BeginSettle.discriminator()],
581            [0x37, 0x13],             // finalize index, little-endian
582            hex!("0807060504030201"), // auction id, little-endian
583            [0x00],                   // order count
584        ];
585        let BeginSettleInput {
586            finalize_ix_index,
587            auction_id,
588            solver_account,
589            instructions_sysvar_account,
590            orders,
591            state_pda_account,
592        } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
593        assert_eq!(finalize_ix_index, 0x1337);
594        assert_eq!(auction_id, 0x0102_0304_0506_0708);
595        assert_eq!(instructions_sysvar_account.address(), &sysvar);
596        assert_eq!(orders.iter().count(), 0);
597        assert_eq!(state_pda_account.address(), &state);
598        assert_eq!(solver_account.address(), &solver);
599    }
600
601    #[test]
602    fn begin_settle_input_rejects_different_discriminator() {
603        let data = ix_data![
604            [SettlementInstruction::FinalizeSettle.discriminator()],
605            [0, 0], // finalize index
606        ];
607        let accounts: [AccountView; 0] = [];
608        assert_eq!(
609            BeginSettleInput::parse(&data, &accounts).err(),
610            Some(ProgramError::InvalidInstructionData),
611        );
612    }
613
614    #[test]
615    fn begin_settle_input_rejects_empty_accounts() {
616        let data = ix_data![
617            [SettlementInstruction::BeginSettle.discriminator()],
618            [0, 0], // finalize index
619        ];
620        let accounts: [AccountView; 0] = [];
621        assert_eq!(
622            BeginSettleInput::parse(&data, &accounts).err(),
623            Some(ProgramError::NotEnoughAccountKeys),
624        );
625    }
626
627    #[test]
628    fn begin_settle_input_rejects_missing_auction_id() {
629        // The body carries the finalize index but fewer than eight bytes for the
630        // auction id, so parsing can't recover it.
631        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
632        let data = ix_data![
633            [SettlementInstruction::BeginSettle.discriminator()],
634            [0, 0],             // finalize index
635            [0x01, 0x02, 0x03], // a partial (3-byte) auction id
636        ];
637        assert_eq!(
638            BeginSettleInput::parse(&data, &accounts).err(),
639            Some(ProgramError::InvalidInstructionData),
640        );
641    }
642
643    #[test]
644    fn begin_settle_input_pairs_orders_with_their_accounts() {
645        let sysvar = pubkey_from_seed("sysvar");
646        let state = pubkey_from_seed("state pda");
647        let spl_token_program = pubkey_from_seed("spl token program");
648        let token_2022_program = pubkey_from_seed("token 2022 program");
649        let solver = pubkey_from_seed("solver");
650        let order_pda = pubkey_from_seed("order pda");
651        let sell_token = pubkey_from_seed("sell token");
652        let accounts = [
653            fake_account(solver),
654            fake_account(sysvar),
655            fake_account(state),
656            fake_account(spl_token_program),
657            fake_account(token_2022_program),
658            fake_account(order_pda),
659            fake_account(sell_token),
660        ];
661        let data = ix_data![
662            [SettlementInstruction::BeginSettle.discriminator()],
663            [0x37, 0x13],             // finalize index, little-endian
664            AUCTION_ID.to_le_bytes(), // auction id, little-endian
665            [0x01],                   // order count
666            [0x00],                   // that order's transfer count
667        ];
668        let BeginSettleInput {
669            finalize_ix_index,
670            auction_id,
671            solver_account,
672            instructions_sysvar_account,
673            orders,
674            state_pda_account,
675        } = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
676        assert_eq!(finalize_ix_index, 0x1337);
677        assert_eq!(auction_id, AUCTION_ID);
678        assert_eq!(instructions_sysvar_account.address(), &sysvar);
679        assert_eq!(state_pda_account.address(), &state);
680        assert_eq!(solver_account.address(), &solver);
681
682        let mut orders = orders.iter();
683        let order = orders.next().expect("one settled order");
684        assert_eq!(order.order_pda.address(), &order_pda);
685        assert_eq!(order.sell_token_account.address(), &sell_token);
686        assert_eq!(order.destinations.len(), 0);
687        assert!(orders.next().is_none());
688    }
689
690    #[test]
691    fn begin_settle_input_parses_transfers() {
692        let sysvar = pubkey_from_seed("sysvar");
693        let state = pubkey_from_seed("state pda");
694        let spl_token_program = pubkey_from_seed("spl token program");
695        let token_2022_program = pubkey_from_seed("token 2022 program");
696        let solver = pubkey_from_seed("solver");
697        let order_pda = pubkey_from_seed("order pda");
698        let sell_token = pubkey_from_seed("sell token");
699        let dest0 = pubkey_from_seed("destination 0");
700        let dest1 = pubkey_from_seed("destination 1");
701        let accounts = [
702            fake_account(solver),
703            fake_account(sysvar),
704            fake_account(state),
705            fake_account(spl_token_program),
706            fake_account(token_2022_program),
707            fake_account(order_pda),
708            fake_account(sell_token),
709            fake_account(dest0),
710            fake_account(dest1),
711        ];
712        let data = ix_data![
713            [SettlementInstruction::BeginSettle.discriminator()],
714            [0x37, 0x13],             // finalize index, little-endian
715            AUCTION_ID.to_le_bytes(), // auction id, little-endian
716            [0x01],                   // order count
717            [0x02],                   // transfer count
718            0x1122u64.to_le_bytes(),
719            0x3344u64.to_le_bytes(),
720        ];
721
722        let BeginSettleInput { orders, .. } =
723            BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
724
725        let mut orders = orders.iter();
726        let order = orders.next().expect("one settled order");
727        assert_eq!(order.order_pda.address(), &order_pda);
728        assert_eq!(order.sell_token_account.address(), &sell_token);
729        let transfers: Vec<(&Address, u64)> = order
730            .destinations
731            .iter()
732            .zip(order.amounts)
733            .map(|(destination, amount)| (destination.address(), u64::from_le_bytes(*amount)))
734            .collect();
735        assert_eq!(transfers, vec![(&dest0, 0x1122), (&dest1, 0x3344)]);
736        assert!(orders.next().is_none());
737    }
738
739    #[test]
740    fn begin_settle_input_pairs_every_order_with_its_sell_token_account() {
741        const ORDER_COUNT: usize = 16;
742
743        let mut expected: Vec<(Address, Address)> = Vec::new();
744        for i in 0..ORDER_COUNT {
745            let order_pda = pubkey_from_seed(&format!("order pda {i}"));
746            let sell_token = pubkey_from_seed(&format!("sell token {i}"));
747            expected.push((order_pda, sell_token));
748        }
749
750        // The fixed accounts (`[0xff..]` down to `[0xfb..]`) differ from every
751        // order/token address above.
752        let mut accounts = vec![
753            fake_account_from_array([0xff; 32]),
754            fake_account_from_array([0xfe; 32]),
755            fake_account_from_array([0xfd; 32]),
756            fake_account_from_array([0xfc; 32]),
757            fake_account_from_array([0xfb; 32]),
758        ];
759        for &(order_pda, sell_token) in &expected {
760            accounts.push(fake_account(order_pda));
761            accounts.push(fake_account(sell_token));
762        }
763        // Grouped data: discriminator, finalize index, auction id, order count,
764        // then all transfer counts (every order has zero transfers).
765        let data = ix_data![
766            [SettlementInstruction::BeginSettle.discriminator()],
767            [0x37, 0x13],             // finalize index, little-endian
768            AUCTION_ID.to_le_bytes(), // auction id, little-endian
769            [ORDER_COUNT as u8],      // order count
770            [0u8; ORDER_COUNT],
771        ];
772
773        let parsed = BeginSettleInput::parse(&data, &accounts).expect("parse should succeed");
774
775        let actual: Vec<(Address, Address)> = parsed
776            .orders
777            .iter()
778            .map(|order| {
779                assert_eq!(order.destinations.len(), 0);
780                (
781                    *order.order_pda.address(),
782                    *order.sell_token_account.address(),
783                )
784            })
785            .collect();
786        assert_eq!(actual, expected);
787    }
788
789    #[test]
790    fn begin_settle_input_rejects_account_count_mismatch() {
791        // The body declares one order with no transfers, which needs exactly two
792        // order accounts (its order PDA and sell token account). Only one order
793        // account is supplied after the fixed accounts, so the number of accounts
794        // doesn't match the `2n + T` the body implies.
795        let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 1 }>();
796        let data = ix_data![
797            [SettlementInstruction::BeginSettle.discriminator()],
798            [0, 0],                   // finalize index
799            AUCTION_ID.to_le_bytes(), // auction id
800            [0x01],                   // order count
801            [0x00],                   // the order's transfer count
802        ];
803        assert_eq!(
804            BeginSettleInput::parse(&data, &accounts).err(),
805            Some(SettlementError::AccountCountNotMatchingOrderCount.into()),
806        );
807    }
808
809    #[test]
810    fn begin_settle_input_rejects_counts_not_summing_to_destinations() {
811        // One order whose two destination accounts (plus its order PDA and sell
812        // token account) make the lengths recover T = 2 transfers, but the
813        // transfer-count byte claims only one.
814        let accounts = fake_sequential_accounts::<{ FIXED_ACCOUNTS + 4 }>();
815        let data = ix_data![
816            [SettlementInstruction::BeginSettle.discriminator()],
817            [0, 0],                   // finalize index
818            AUCTION_ID.to_le_bytes(), // auction id
819            [0x01],                   // order count
820            [0x01],                   // count says one, but two amounts/destinations exist
821            0u64.to_le_bytes(),
822            0u64.to_le_bytes(),
823        ];
824        assert_eq!(
825            BeginSettleInput::parse(&data, &accounts).err(),
826            Some(SettlementError::TransferCountMismatch.into()),
827        );
828    }
829
830    #[test]
831    fn begin_settle_input_rejects_missing_order_count() {
832        // The body carries the finalize index and auction id but no order-count
833        // byte, so the pull layout can't even begin to be parsed.
834        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
835        let data = ix_data![
836            [SettlementInstruction::BeginSettle.discriminator()],
837            [0, 0],                   // finalize index
838            AUCTION_ID.to_le_bytes(), // auction id
839        ];
840        assert_eq!(
841            BeginSettleInput::parse(&data, &accounts).err(),
842            Some(ProgramError::InvalidInstructionData),
843        );
844    }
845
846    #[test]
847    fn begin_settle_input_rejects_body_too_short_for_counts() {
848        // The order count claims two orders, but only one transfer-count byte
849        // follows, so the counts can't be split off.
850        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
851        let data = ix_data![
852            [SettlementInstruction::BeginSettle.discriminator()],
853            [0, 0],                   // finalize index
854            AUCTION_ID.to_le_bytes(), // auction id
855            [0x02],                   // order count: two orders...
856            [0x00],                   // ...but only one transfer-count byte
857        ];
858        assert_eq!(
859            BeginSettleInput::parse(&data, &accounts).err(),
860            Some(ProgramError::InvalidInstructionData),
861        );
862    }
863
864    #[test]
865    fn begin_settle_input_rejects_partial_amount() {
866        // One order with no transfers, but four trailing bytes that don't form a
867        // whole `u64` amount.
868        let accounts = fake_sequential_accounts::<FIXED_ACCOUNTS>();
869        let data = ix_data![
870            [SettlementInstruction::BeginSettle.discriminator()],
871            [0, 0],                   // finalize index
872            AUCTION_ID.to_le_bytes(), // auction id
873            [0x01],                   // order count
874            [0x00],                   // transfer count
875            [0x11, 0x22, 0x33, 0x44], // a partial (4-byte) amount
876        ];
877        assert_eq!(
878            BeginSettleInput::parse(&data, &accounts).err(),
879            Some(ProgramError::InvalidInstructionData),
880        );
881    }
882}