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