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