Skip to main content

cow_settlement_interface/instruction/settle/
finalize.rs

1//! Off-chain builder and input parsing for the `FinalizeSettle` 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::{recover_discriminator, SettlementError, SettlementInstruction};
11
12use super::{
13    recover_counterpart, MaybeMint, TokenProgram, INSTRUCTIONS_SYSVAR_ID, MINT_PLACEHOLDER,
14};
15
16/// The number of fixed accounts every `FinalizeSettle` carries before its push
17/// accounts: the instructions sysvar, the settlement state PDA, and one slot per
18/// supported token program.
19pub const FINALIZE_FIXED_ACCOUNTS: usize = 4;
20
21/// The number of accounts each `FinalizeSettle` push carries:
22/// `[source_buffer, destination, mint]`.
23pub const FINALIZE_PUSH_ACCOUNTS: usize = 3;
24
25/// Split the instruction bytes from `FinalizeSettle` that remain after all
26/// constant-size data has been extracted into the per-push bump bytes and the
27/// raw little-endian amount chunks.
28fn split_push_bytes(body: &[u8]) -> Result<(&[u8], &[[u8; 8]]), ProgramError> {
29    // The body is `9 * n` bytes: `n` bump bytes followed by `n` little-endian
30    // `u64` amounts. A body that isn't a whole number of these 9-byte pushes
31    // can't be parsed into the push layout (and would otherwise leave `bumps`
32    // and `amounts` with mismatched lengths).
33    if !body.len().is_multiple_of(9) {
34        return Err(ProgramError::InvalidInstructionData);
35    }
36    let push_count = body.len() / 9;
37    let (bumps, amount_bytes) = body.split_at(push_count);
38    let (amounts, []) = amount_bytes.as_chunks::<8>() else {
39        return Err(ProgramError::InvalidInstructionData);
40    };
41    Ok((bumps, amounts))
42}
43
44/// The per-push data — each push's source-buffer `bump` and the little-endian
45/// `amount` it pays, as parallel slices of equal length in push order — carried
46/// by a `FinalizeSettle` instruction and recovered from its full instruction
47/// data alone (discriminator included, as returned by instruction
48/// introspection).
49///
50/// This function doesn't otherwise check that the instruction is consistent
51/// with a `FinalizeSettle` instruction. For example, the discriminator field is
52/// ignored.
53pub fn finalize_push_data(instruction_data: &[u8]) -> Result<(&[u8], &[[u8; 8]]), ProgramError> {
54    let (_discriminator, rest) = recover_discriminator(instruction_data)?;
55    let (_begin_ix_index, body) = recover_counterpart(rest)?;
56    split_push_bytes(body)
57}
58
59/// Builder for a `FinalizeSettle` instruction pushing the funds described by the
60/// parallel lists:
61/// - `source_buffers[i]` is the account the funds come from: the buffer token
62///   account of the order's buy mint, or the native SOL buffer for an order
63///   buying native SOL,
64/// - `destinations[i]` is the account the funds go to (an order's buy token
65///   account),
66/// - `mints[i]` is the buy mint if the push uses `TransferChecked`, or `None`
67///   for plain `Transfer` and for native SOL pushes, which ignore
68///   it,
69/// - `bumps[i]` is the canonical bump of `source_buffers[i]`, so the program
70///   re-derives that PDA with one hash instead of searching,
71/// - `amounts[i]` is the amount to push, in the buy mint's units or in lamports
72///   for a native SOL push.
73///
74/// This instruction comes in pair with a `BeginSettle` instruction. The slices
75/// are assumed to be built in parallel with the order information specified in
76/// `BeginSettle`. Notably, (1) the source buffer is the one corresponding to
77/// the order's buy token, and (2) the destinations must be the buy token
78/// accounts specified in the corresponding order.
79/// The slices are assumed to have the same length but this is not enforced by
80/// the builder.
81///
82/// Wire format (with `n` total pushes):
83/// `[discriminator=1][begin_ix_index: u16 LE][bump: u8 ×n][amount: u64 LE ×n]`.
84/// Required accounts:
85/// `[instructions_sysvar (R), state_pda (R), spl_token_program (R),
86/// token_2022_program (R)]` followed, per push, by `[source_buffer (W),
87/// destination (W), mint (R)]`. The two token programs are the slots
88/// [`TokenProgram::ALL`] describes, there to name the programs this
89/// instruction's pushes are issued against; the matching `BeginSettle` carries
90/// the ones its pulls need.
91/// A `mint` holding [`MINT_PLACEHOLDER`] makes the push a
92/// plain `Transfer`; any other mint makes it a `TransferChecked`.
93///
94/// `FinalizeSettle` only executes the transfers. Every push is validated by
95/// `BeginSettle`, which reads this instruction through introspection.
96#[cfg_attr(any(test, feature = "test-fixtures"), derive(Default))]
97pub struct FinalizeSettle<'a> {
98    pub program_id: Pubkey,
99    pub state_pda: Pubkey,
100    pub begin_ix_index: u16,
101    /// The only token program this settlement's transfers are issued against,
102    /// or `None` to name every supported one; see [`TokenProgram::ALL`].
103    pub only_token_program: Option<TokenProgram>,
104    pub source_buffers: &'a [Pubkey],
105    pub destinations: &'a [Pubkey],
106    pub mints: &'a [Option<Pubkey>],
107    pub bumps: &'a [u8],
108    pub amounts: &'a [u64],
109}
110
111impl From<FinalizeSettle<'_>> for Instruction {
112    fn from(builder: FinalizeSettle<'_>) -> Self {
113        let FinalizeSettle {
114            program_id,
115            state_pda,
116            begin_ix_index,
117            only_token_program,
118            source_buffers,
119            destinations,
120            mints,
121            bumps,
122            amounts,
123        } = builder;
124
125        let data: Vec<u8> = core::iter::once(SettlementInstruction::FinalizeSettle.discriminator())
126            .chain(begin_ix_index.to_le_bytes())
127            .chain(bumps.iter().copied())
128            .chain(amounts.iter().flat_map(|amount| amount.to_le_bytes()))
129            .collect();
130
131        let mut accounts = vec![
132            AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false),
133            AccountMeta::new_readonly(state_pda, false),
134        ];
135        // One account per supported token program. If `only_token_program`,
136        // replace the other program in the instruction with an account that's
137        // already present (and so doesn't take extra space in the tx).
138        accounts.extend(TokenProgram::ALL.map(|program| {
139            let address = if only_token_program.is_none_or(|only| only == program) {
140                program.address()
141            } else {
142                INSTRUCTIONS_SYSVAR_ID
143            };
144            AccountMeta::new_readonly(address, false)
145        }));
146        for ((source, destination), mint) in source_buffers.iter().zip(destinations).zip(mints) {
147            accounts.push(AccountMeta::new(*source, false));
148            accounts.push(AccountMeta::new(*destination, false));
149            accounts.push(AccountMeta::new_readonly(
150                mint.unwrap_or(MINT_PLACEHOLDER),
151                false,
152            ));
153        }
154
155        Instruction {
156            program_id,
157            accounts,
158            data,
159        }
160    }
161}
162
163/// A single fund push parsed from `FinalizeSettle`: move `amount` from
164/// `source_buffer` to `destination`. `bump` is `source_buffer`'s claimed
165/// canonical bump — of the buy mint's buffer PDA, or of the native SOL buffer
166/// for a native SOL push — which the program re-derives against.
167#[derive(Debug, PartialEq, Eq)]
168pub struct Push<'a, A> {
169    pub source_buffer: &'a A,
170    pub destination: &'a A,
171    pub mint: MaybeMint<'a, A>,
172    pub bump: u8,
173    pub amount: u64,
174}
175
176/// Struct storing accounts, bumps, and amounts from parsing the input of
177/// `FinalizeSettle`: one `[source_buffer, destination, mint]` account triple
178/// per push, parallel to `bumps` and `amounts`. The parsing step that created
179/// this struct guarantees all three have the same length.
180pub struct Pushes<'a, A> {
181    push_accounts: &'a [[A; FINALIZE_PUSH_ACCOUNTS]],
182    bumps: &'a [u8],
183    /// One push amount (little-endian `u64`) per push, parallel to `bumps`.
184    amounts: &'a [[u8; 8]],
185}
186
187impl<'a, A> Pushes<'a, A> {
188    /// Returns an iterator yielding one [`Push`] per step.
189    pub fn iter(&self) -> impl Iterator<Item = Push<'a, A>> + '_ {
190        self.push_accounts
191            .iter()
192            .zip(self.bumps)
193            .zip(self.amounts)
194            .map(
195                |(([source_buffer, destination, mint], &bump), amount)| Push {
196                    source_buffer,
197                    destination,
198                    mint: MaybeMint::new(mint),
199                    bump,
200                    amount: u64::from_le_bytes(*amount),
201                },
202            )
203    }
204}
205
206/// Parsed inputs (instruction-data fields + relevant accounts) of a
207/// `FinalizeSettle` instruction.
208///
209/// Strictly the raw extracted form. Fields are read from `instruction_data` and
210/// `accounts` but **not validated** against runtime context except confirming
211/// that the discriminator matches the desired input and that the number of
212/// accounts and amounts is consistent.
213pub struct FinalizeSettleInput<'a, A> {
214    pub begin_ix_index: u16,
215    pub instructions_sysvar_account: &'a A,
216    pub state_pda_account: &'a A,
217    pub pushes: Pushes<'a, A>,
218}
219
220/// This implementation defines how instruction bytes and accounts are laid out
221/// in the transaction. It's the source of truth for deciding where the data
222/// is stored.
223impl<'a, A> InstructionInputParsing<'a, A> for FinalizeSettleInput<'a, A> {
224    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::FinalizeSettle;
225
226    fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
227        let (begin_ix_index, body) = recover_counterpart(instruction_data)?;
228
229        // The two token-program slots are skipped rather than read since they are only
230        // used for program invocation.
231        let [instructions_sysvar_account, state_pda_account, _spl_token_program_account, _token_2022_program_account, push_accounts @ ..] =
232            accounts
233        else {
234            return Err(ProgramError::NotEnoughAccountKeys);
235        };
236
237        let (bumps, amounts) = split_push_bytes(body)?;
238        let push_count = bumps.len();
239
240        // Each push contributes a source buffer, a destination, and a mint, so
241        // the push-account count is `3 * n`.
242        let (push_triples, []) = push_accounts.as_chunks::<FINALIZE_PUSH_ACCOUNTS>() else {
243            return Err(SettlementError::AccountCountNotMatchingPushCount.into());
244        };
245        if push_triples.len() != push_count {
246            return Err(SettlementError::AccountCountNotMatchingPushCount.into());
247        }
248
249        Ok(Self {
250            begin_ix_index,
251            instructions_sysvar_account,
252            state_pda_account,
253            pushes: Pushes {
254                push_accounts: push_triples,
255                bumps,
256                amounts,
257            },
258        })
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::fixtures::pubkey_from_seed;
266    use crate::instruction::fixtures::{
267        fake_account, fake_account_from_array, fake_sequential_accounts,
268    };
269    use crate::instruction::settle::tests::{ix_data, mint_address};
270    use crate::instruction::tests::assert_readonly_nonsigner;
271    use crate::token_program::TokenProgram;
272    use hex_literal::hex;
273    use proptest::prelude::*;
274    use solana_account_view::AccountView;
275    use solana_address::Address;
276
277    #[test]
278    fn finalize_fixed_accounts_matches_builder() {
279        // A no-push finalize carries exactly the fixed accounts, so the constant
280        // must equal the account count the builder emits with no pushes.
281        let ix = Instruction::from(FinalizeSettle {
282            program_id: pubkey_from_seed("program id"),
283            state_pda: pubkey_from_seed("state pda"),
284            ..Default::default()
285        });
286        assert_eq!(ix.accounts.len(), FINALIZE_FIXED_ACCOUNTS);
287    }
288
289    #[test]
290    fn expected_encoding_finalize_settle_no_pushes() {
291        let program_id = pubkey_from_seed("program id");
292        let state_pda = pubkey_from_seed("state pda");
293        let Instruction {
294            program_id: ix_program_id,
295            accounts,
296            data,
297        } = FinalizeSettle {
298            program_id,
299            state_pda,
300            begin_ix_index: 0x1337,
301            ..Default::default()
302        }
303        .into();
304        assert_eq!(ix_program_id, program_id);
305        assert_eq!(
306            data,
307            ix_data![
308                [SettlementInstruction::FinalizeSettle.discriminator()],
309                hex!("3713"), // counterpart index (little-endian)
310            ],
311        );
312        // No orders: the fixed accounts (sysvar, state PDA, and a slot per token
313        // program). They are all generic accounts that don't play an active role
314        // in the base instruction (the state PDA CPI signature isn't relevant
315        // here). This settlement carries only the legacy program, so Token-2022's
316        // slot holds the placeholder.
317        assert_eq!(accounts.len(), FINALIZE_FIXED_ACCOUNTS);
318        assert_readonly_nonsigner(&accounts[0], INSTRUCTIONS_SYSVAR_ID);
319        assert_readonly_nonsigner(&accounts[1], state_pda);
320        assert_readonly_nonsigner(&accounts[2], TokenProgram::SplToken.address());
321        assert_readonly_nonsigner(&accounts[3], TokenProgram::Token2022.address());
322    }
323
324    /// The token-program slots are the addresses the settlement's
325    /// `only_token_program` names, in [`TokenProgram::ALL`] order, so a
326    /// settlement can name both programs — or leave either one out.
327    #[test]
328    fn finalize_settle_carries_the_token_program_slots_it_is_given() {
329        for (only_token_program, expected) in [
330            (
331                None,
332                [
333                    TokenProgram::SplToken.address(),
334                    TokenProgram::Token2022.address(),
335                ],
336            ),
337            (
338                Some(TokenProgram::SplToken),
339                [TokenProgram::SplToken.address(), INSTRUCTIONS_SYSVAR_ID],
340            ),
341            (
342                Some(TokenProgram::Token2022),
343                [INSTRUCTIONS_SYSVAR_ID, TokenProgram::Token2022.address()],
344            ),
345        ] {
346            let ix = Instruction::from(FinalizeSettle {
347                program_id: Pubkey::new_unique(),
348                state_pda: Pubkey::new_unique(),
349                only_token_program,
350                ..Default::default()
351            });
352            let slots: Vec<Pubkey> = ix.accounts[2..].iter().map(|meta| meta.pubkey).collect();
353            assert_eq!(
354                slots, expected,
355                "{only_token_program:?} should name just the programs it settles against",
356            );
357        }
358    }
359
360    #[test]
361    fn finalize_settle_encodes_pushes() {
362        let program_id = pubkey_from_seed("program id");
363        let state_pda = pubkey_from_seed("state pda");
364        let source_a = Pubkey::new_from_array([0x01; 32]);
365        let dest_a = Pubkey::new_from_array([0x02; 32]);
366        let source_b = Pubkey::new_from_array([0x03; 32]);
367        let dest_b = Pubkey::new_from_array([0x04; 32]);
368        let mint_a = Pubkey::new_from_array([0x05; 32]);
369
370        let ix = Instruction::from(FinalizeSettle {
371            program_id,
372            state_pda,
373            begin_ix_index: 0x1337,
374            source_buffers: &[source_a, source_b],
375            destinations: &[dest_a, dest_b],
376            mints: &[Some(mint_a), None],
377            bumps: &[0xa1, 0xb1],
378            amounts: &[0x01020304, 0x05060708],
379            ..Default::default()
380        });
381
382        assert_eq!(
383            ix.data,
384            ix_data![
385                [SettlementInstruction::FinalizeSettle.discriminator()],
386                hex!("3713"), // counterpart index (little-endian)
387                [0xa1, 0xb1], // bumps
388                // amounts (little-endian)
389                hex!("0403020100000000"),
390                hex!("0807060500000000"),
391            ],
392        );
393
394        let actual: Vec<Pubkey> = ix.accounts.iter().map(|meta| meta.pubkey).collect();
395        assert_eq!(
396            actual,
397            vec![
398                INSTRUCTIONS_SYSVAR_ID,
399                state_pda,
400                TokenProgram::SplToken.address(),
401                TokenProgram::Token2022.address(),
402                source_a,
403                dest_a,
404                mint_a,
405                source_b,
406                dest_b,
407                MINT_PLACEHOLDER,
408            ],
409        );
410        // The fixed accounts and mints are read-only; the source buffers and
411        // destinations are writable for the transfers.
412        let writable: Vec<Pubkey> = ix
413            .accounts
414            .iter()
415            .filter(|meta| meta.is_writable)
416            .map(|meta| meta.pubkey)
417            .collect();
418        assert_eq!(writable, vec![source_a, dest_a, source_b, dest_b]);
419        assert!(ix.accounts.iter().all(|meta| !meta.is_signer));
420    }
421
422    #[test]
423    fn finalize_settle_input_parses_no_pushes() {
424        let sysvar = pubkey_from_seed("sysvar");
425        let state = pubkey_from_seed("state pda");
426        let spl_token_program = pubkey_from_seed("spl token program");
427        let token_2022_program = pubkey_from_seed("token 2022 program");
428        let accounts = [
429            fake_account(sysvar),
430            fake_account(state),
431            fake_account(spl_token_program),
432            fake_account(token_2022_program),
433        ];
434        let data = ix_data![
435            [SettlementInstruction::FinalizeSettle.discriminator()],
436            [0x37, 0x13], // begin index, little-endian
437        ];
438        let FinalizeSettleInput {
439            begin_ix_index,
440            instructions_sysvar_account,
441            state_pda_account,
442            pushes,
443        } = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
444        assert_eq!(begin_ix_index, 0x1337);
445        assert_eq!(instructions_sysvar_account.address(), &sysvar);
446        assert_eq!(state_pda_account.address(), &state);
447        assert_eq!(pushes.iter().count(), 0);
448    }
449
450    #[test]
451    fn finalize_settle_input_parses_pushes() {
452        let sysvar = pubkey_from_seed("sysvar");
453        let state = pubkey_from_seed("state pda");
454        let spl_token_program = pubkey_from_seed("spl token program");
455        let token_2022_program = pubkey_from_seed("token 2022 program");
456        // The same source buffer funds both pushes: parsing makes no uniqueness
457        // assumption about source buffers.
458        let source = pubkey_from_seed("source buffer");
459        let dest0 = pubkey_from_seed("destination 0");
460        let dest1 = pubkey_from_seed("destination 1");
461        let mint0 = pubkey_from_seed("mint 0");
462        let mint1 = pubkey_from_seed("mint 1");
463        let accounts = [
464            fake_account(sysvar),
465            fake_account(state),
466            fake_account(spl_token_program),
467            fake_account(token_2022_program),
468            fake_account(source),
469            fake_account(dest0),
470            fake_account(mint0),
471            fake_account(source),
472            fake_account(dest1),
473            fake_account(mint1),
474        ];
475        let data = ix_data![
476            [SettlementInstruction::FinalizeSettle.discriminator()],
477            [0x13, 0x37], // begin index
478            [0xfe, 0xfd], // bumps
479            0x1122u64.to_le_bytes(),
480            0x3344u64.to_le_bytes(),
481        ];
482
483        let FinalizeSettleInput { pushes, .. } =
484            FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
485
486        let parsed: Vec<(&Address, &Address, &Address, u8, u64)> = pushes
487            .iter()
488            .map(|push| {
489                (
490                    push.source_buffer.address(),
491                    push.destination.address(),
492                    mint_address(push.mint),
493                    push.bump,
494                    push.amount,
495                )
496            })
497            .collect();
498        assert_eq!(
499            parsed,
500            vec![
501                (&source, &dest0, &mint0, 0xfe, 0x1122),
502                (&source, &dest1, &mint1, 0xfd, 0x3344),
503            ],
504        );
505    }
506
507    #[test]
508    fn finalize_settle_input_parses_many_pushes() {
509        const PUSH_COUNT: usize = 16;
510
511        struct ExpectedPush {
512            source: Address,
513            dest: Address,
514            mint: Address,
515            bump: u8,
516            amount: u64,
517        }
518        let mut expected: Vec<ExpectedPush> = Vec::new();
519        for i in 0..PUSH_COUNT {
520            let source = pubkey_from_seed(&format!("source buffer {i}"));
521            let dest = pubkey_from_seed(&format!("destination {i}"));
522            let mint = pubkey_from_seed(&format!("mint {i}"));
523            let bump = (i + 2 * PUSH_COUNT) as u8;
524            let amount = u64::from_le_bytes([(i + 3 * PUSH_COUNT) as u8; 8]);
525            expected.push(ExpectedPush {
526                source,
527                dest,
528                mint,
529                bump,
530                amount,
531            });
532        }
533
534        // The fixed accounts (`[0xff..]` down to `[0xfc..]`) differ from every
535        // source/destination address above.
536        let mut accounts = vec![
537            fake_account_from_array([0xff; 32]),
538            fake_account_from_array([0xfe; 32]),
539            fake_account_from_array([0xfd; 32]),
540            fake_account_from_array([0xfc; 32]),
541        ];
542        let mut bump_bytes = Vec::new();
543        let mut amount_bytes = Vec::new();
544        for push in &expected {
545            accounts.push(fake_account(push.source));
546            accounts.push(fake_account(push.dest));
547            accounts.push(fake_account(push.mint));
548            bump_bytes.push(push.bump);
549            amount_bytes.extend_from_slice(&push.amount.to_le_bytes());
550        }
551        let data = ix_data![
552            [SettlementInstruction::FinalizeSettle.discriminator()],
553            [0x13, 0x37], // begin index
554            bump_bytes,
555            amount_bytes,
556        ];
557
558        let parsed = FinalizeSettleInput::parse(&data, &accounts).expect("parse should succeed");
559        let pushes: Vec<_> = parsed.pushes.iter().collect();
560
561        assert_eq!(pushes.len(), PUSH_COUNT);
562        for (push, expected) in pushes.iter().zip(&expected) {
563            assert_eq!(push.source_buffer.address(), &expected.source);
564            assert_eq!(push.destination.address(), &expected.dest);
565            assert_eq!(mint_address(push.mint), &expected.mint);
566            assert_eq!(push.bump, expected.bump);
567            assert_eq!(push.amount, expected.amount);
568        }
569    }
570
571    #[test]
572    fn finalize_settle_input_rejects_different_discriminator() {
573        let data = ix_data![
574            [SettlementInstruction::BeginSettle.discriminator()],
575            [0, 0], // begin index
576        ];
577        let accounts: [AccountView; 0] = [];
578        assert_eq!(
579            FinalizeSettleInput::parse(&data, &accounts).err(),
580            Some(ProgramError::InvalidInstructionData),
581        );
582    }
583
584    #[test]
585    fn finalize_settle_input_rejects_empty_accounts() {
586        let data = ix_data![
587            [SettlementInstruction::FinalizeSettle.discriminator()],
588            [0, 0], // begin index
589        ];
590        let accounts: [AccountView; 0] = [];
591        assert_eq!(
592            FinalizeSettleInput::parse(&data, &accounts).err(),
593            Some(ProgramError::NotEnoughAccountKeys),
594        );
595    }
596
597    #[test]
598    fn finalize_settle_input_rejects_account_count_mismatch() {
599        // One push (a bump byte then a `u64` amount) needs exactly three push
600        // accounts: its source buffer, destination, and mint.
601        let data: Vec<u8> = ix_data![
602            [SettlementInstruction::FinalizeSettle.discriminator()],
603            [13, 37], // begin index
604            [0xff],   // the push's bump
605            31337u64.to_le_bytes(),
606        ];
607
608        let too_few = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 2 }>();
609        assert_eq!(
610            FinalizeSettleInput::parse(&data, &too_few).err(),
611            Some(SettlementError::AccountCountNotMatchingPushCount.into()),
612        );
613
614        let too_many = fake_sequential_accounts::<{ FINALIZE_FIXED_ACCOUNTS + 4 }>();
615        assert_eq!(
616            FinalizeSettleInput::parse(&data, &too_many).err(),
617            Some(SettlementError::AccountCountNotMatchingPushCount.into()),
618        );
619    }
620
621    #[test]
622    fn finalize_settle_input_rejects_partial_push() {
623        // Four trailing bytes: not a whole number of 9-byte pushes (a bump plus a
624        // `u64` amount), so the body can't be parsed into the push layout.
625        let accounts = fake_sequential_accounts::<FINALIZE_FIXED_ACCOUNTS>();
626        let data = ix_data![
627            [SettlementInstruction::FinalizeSettle.discriminator()],
628            [37, 13],                 // begin index
629            [0xff],                   // the push's bump
630            [0x11, 0x22, 0x33, 0x44], // a partial push (4 bytes)
631        ];
632        assert_eq!(
633            FinalizeSettleInput::parse(&data, &accounts).err(),
634            Some(ProgramError::InvalidInstructionData),
635        );
636    }
637
638    #[test]
639    fn finalize_push_data_extracts_bumps_and_amounts() {
640        let bumps = [0xa1, 0xb1];
641        let amounts = [0x0102, 0x0304];
642        let ix = Instruction::from(FinalizeSettle {
643            program_id: pubkey_from_seed("program id"),
644            state_pda: pubkey_from_seed("state pda"),
645            begin_ix_index: 0x1337,
646            source_buffers: &[
647                pubkey_from_seed("source buffer 0"),
648                pubkey_from_seed("source buffer 1"),
649            ],
650            destinations: &[
651                pubkey_from_seed("destination 0"),
652                pubkey_from_seed("destination 1"),
653            ],
654            mints: &[None, None],
655            bumps: &bumps,
656            amounts: &amounts,
657            ..Default::default()
658        });
659
660        let (bumps, amounts) = finalize_push_data(&ix.data).expect("valid finalize data");
661        assert_eq!(bumps, [0xa1, 0xb1]);
662        assert_eq!(amounts, [0x0102u64.to_le_bytes(), 0x0304u64.to_le_bytes()]);
663    }
664
665    #[test]
666    fn finalize_push_data_handles_no_pushes() {
667        let ix = Instruction::from(FinalizeSettle {
668            program_id: pubkey_from_seed("program id"),
669            state_pda: pubkey_from_seed("state pda"),
670            ..Default::default()
671        });
672        let (bumps, amounts) = finalize_push_data(&ix.data).expect("valid empty finalize data");
673        assert!(bumps.is_empty());
674        assert!(amounts.is_empty());
675    }
676
677    #[test]
678    fn finalize_push_data_rejects_incorrect_bytes() {
679        let mut ix = Instruction::from(FinalizeSettle {
680            program_id: pubkey_from_seed("program id"),
681            state_pda: pubkey_from_seed("state pda"),
682            source_buffers: &[pubkey_from_seed("source buffer")],
683            destinations: &[pubkey_from_seed("destination")],
684            mints: &[None],
685            bumps: &[0xff],
686            amounts: &[31337],
687            ..Default::default()
688        });
689        ix.data.pop();
690        assert_eq!(
691            finalize_push_data(&ix.data).err(),
692            Some(ProgramError::InvalidInstructionData),
693        );
694    }
695
696    /// An arbitrary well-formed `FinalizeSettle` instruction with `push_count`
697    /// pushes.
698    fn arb_finalize_instruction(
699        push_count: impl Into<prop::collection::SizeRange>,
700    ) -> impl Strategy<Value = Instruction> {
701        (
702            any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
703            any::<[u8; 32]>().prop_map(Pubkey::new_from_array),
704            any::<u16>(),
705            crate::instruction::settle::fixtures::arb_pushes(push_count),
706        )
707            .prop_map(|(program_id, state_pda, begin_ix_index, pushes)| {
708                Instruction::from(FinalizeSettle {
709                    program_id,
710                    state_pda,
711                    begin_ix_index,
712                    source_buffers: &pushes.source_buffers,
713                    destinations: &pushes.destinations,
714                    mints: &pushes.mints,
715                    bumps: &pushes.bumps,
716                    amounts: &pushes.amounts,
717                    ..Default::default()
718                })
719            })
720    }
721
722    proptest! {
723        /// For any well-formed `FinalizeSettle`, the bumps and amounts
724        /// `finalize_push_data` recovers from the instruction data alone (the way
725        /// `BeginSettle` sees it through introspection) must equal what the full
726        /// `FinalizeSettleInput` parser reads from the same data plus its accounts.
727        #[test]
728        fn finalize_push_data_matches_parser(ix in arb_finalize_instruction(0..=16usize)) {
729            // Recovered from the instruction data alone.
730            let (bumps, amounts) = finalize_push_data(&ix.data).expect("well-formed finalize data");
731            let recovered: Vec<(u8, u64)> = bumps
732                .iter()
733                .copied()
734                .zip(amounts.iter().copied().map(u64::from_le_bytes))
735                .collect();
736
737            // Read by the full parser from the same data plus its accounts.
738            let accounts: Vec<AccountView> =
739                ix.accounts.iter().map(|meta| fake_account(meta.pubkey)).collect();
740            let parsed = FinalizeSettleInput::parse(&ix.data, &accounts)
741                .expect("a well-formed finalize parses");
742            let parsed_pushes: Vec<(u8, u64)> =
743                parsed.pushes.iter().map(|push| (push.bump, push.amount)).collect();
744
745            prop_assert_eq!(recovered, parsed_pushes);
746        }
747    }
748}