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