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