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