Skip to main content

cow_settlement_interface/instruction/
initialize.rs

1//! `Initialize` instruction builder.
2//!
3//! Allocates the singleton settlement state PDA (see [`crate::pda::state`]).
4
5use solana_instruction::{AccountMeta, Instruction};
6use solana_program_error::ProgramError;
7use solana_pubkey::Pubkey;
8
9pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
10
11use super::InstructionInputParsing;
12use crate::SettlementInstruction;
13
14/// Builder for an `Initialize` instruction.
15///
16/// `payer` funds the new account's rent and signs. It is meant to be the
17/// transaction's fee payer: the state is created once at deployment and never
18/// deallocated, so there's no need for a dedicated funding account separate
19/// from whoever pays for the deployment transaction.
20///
21/// `state_pda` must be the canonical PDA returned by
22/// [`crate::pda::state::find_state_pda`]; the program derives the bump itself
23/// and rejects any other address.
24///
25/// `reclaim_authority` is recorded verbatim in the state PDA's data: it's the
26/// account authorized to reclaim rent for buffers. See
27/// [`crate::data::state::StateAccount::reclaim_authority`].
28///
29/// The state account is owned by the settlement program. This instruction
30/// succeeds only once: a second call fails because the account already
31/// exists.
32///
33/// Wire format: `[discriminator=3, reclaim_authority (32 bytes)]`, 33 bytes.
34/// Required accounts: `[payer (W,S), state_pda (W), system_program (R)]`.
35/// The system program must be available for the `CreateAccount` CPI but doesn't
36/// need to sit at that specific position.
37pub struct Initialize {
38    pub program_id: Pubkey,
39    pub payer: Pubkey,
40    pub state_pda: Pubkey,
41    pub reclaim_authority: Pubkey,
42}
43
44impl From<Initialize> for Instruction {
45    fn from(builder: Initialize) -> Self {
46        let mut data = vec![SettlementInstruction::Initialize.discriminator()];
47        data.extend_from_slice(&builder.reclaim_authority.to_bytes());
48        Instruction {
49            program_id: builder.program_id,
50            accounts: vec![
51                AccountMeta::new(builder.payer, true),
52                AccountMeta::new(builder.state_pda, false),
53                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
54            ],
55            data,
56        }
57    }
58}
59
60/// Parsed inputs of an `Initialize` instruction.
61pub struct InitializeInput<'a, A> {
62    pub payer: &'a A,
63    pub state_pda: &'a mut A,
64    pub reclaim_authority: Pubkey,
65}
66
67impl<'a, A> InstructionInputParsing<'a, A> for InitializeInput<'a, A> {
68    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::Initialize;
69
70    fn parse_body(instruction_data: &[u8], accounts: &'a mut [A]) -> Result<Self, ProgramError> {
71        let reclaim_authority: [u8; 32] = instruction_data
72            .try_into()
73            .map_err(|_| ProgramError::InvalidInstructionData)?;
74        let reclaim_authority = Pubkey::new_from_array(reclaim_authority);
75
76        // Accounts: [payer (W,S), state_pda (W), system_program (R)]. The system
77        // program needs to be present for the `CreateAccount` CPI but doesn't
78        // need to be referenced directly and can be at any later position.
79        let [payer, state_pda, _system, ..] = accounts else {
80            return Err(ProgramError::NotEnoughAccountKeys);
81        };
82
83        Ok(Self {
84            payer,
85            state_pda,
86            reclaim_authority,
87        })
88    }
89}
90
91/// Test scaffolding for `Initialize` parsing and handling, shared by this
92/// crate's tests and the settlement program's via the `test-fixtures` feature.
93#[cfg(any(test, feature = "test-fixtures"))]
94pub mod fixtures {
95    use solana_address::Address;
96
97    use super::{Initialize, Instruction};
98
99    /// Number of accounts `Initialize` expects: payer, state PDA, system program.
100    pub const NUM_ACCOUNTS: usize = 3;
101
102    /// `Initialize` instruction data with placeholder addresses, for failure
103    /// cases where the actual addresses don't matter.
104    pub fn initialize_data() -> Vec<u8> {
105        let zero = Address::new_from_array([0; 32]);
106        Instruction::from(Initialize {
107            program_id: zero,
108            payer: zero,
109            state_pda: zero,
110            reclaim_authority: zero,
111        })
112        .data
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::fixtures::{initialize_data, NUM_ACCOUNTS};
119    use super::*;
120    use crate::data::state::EncodedStateAccount;
121    use crate::instruction::fixtures::{fake_account_from_array, fake_sequential_accounts};
122    use crate::instruction::tests::{
123        assert_readonly_nonsigner, assert_writable_nonsigner, assert_writable_signer,
124    };
125    use solana_account_view::AccountView;
126    use solana_address::Address;
127
128    #[test]
129    fn initialize_input_parses_valid_input() {
130        let program_id = Address::new_unique();
131        let payer = fake_account_from_array([1; 32]);
132        let state_pda = fake_account_from_array([2; 32]);
133        let reclaim_authority = Address::new_from_array([4; 32]);
134        let data = Instruction::from(Initialize {
135            program_id,
136            payer: *payer.address(),
137            state_pda: *state_pda.address(),
138            reclaim_authority,
139        })
140        .data;
141
142        let system_program = fake_account_from_array([3; 32]);
143        let mut accounts = [payer, state_pda, system_program];
144
145        let InitializeInput {
146            payer: parsed_payer,
147            state_pda: parsed_state_pda,
148            reclaim_authority: parsed_reclaim_authority,
149        } = InitializeInput::parse(&data, &mut accounts).expect("parse should succeed");
150
151        assert_eq!(parsed_payer.address(), payer.address());
152        assert_eq!(parsed_state_pda.address(), state_pda.address());
153        assert_eq!(parsed_reclaim_authority, reclaim_authority);
154    }
155
156    #[test]
157    fn initialize_input_rejects_long_data() {
158        let mut data = initialize_data();
159        data.push(0); // trailing byte
160        let mut accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
161        assert_eq!(
162            InitializeInput::parse(&data, &mut accounts).err(),
163            Some(ProgramError::InvalidInstructionData),
164        );
165    }
166
167    #[test]
168    fn initialize_input_rejects_short_data() {
169        let mut data = initialize_data();
170        data.pop(); // one byte short of a full reclaim_authority pubkey
171        let mut accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
172        assert_eq!(
173            InitializeInput::parse(&data, &mut accounts).err(),
174            Some(ProgramError::InvalidInstructionData),
175        );
176    }
177
178    #[test]
179    fn initialize_input_rejects_missing_accounts() {
180        let data = initialize_data();
181        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
182        accounts.pop();
183        assert_eq!(
184            InitializeInput::parse(&data, &mut accounts).err(),
185            Some(ProgramError::NotEnoughAccountKeys),
186        );
187    }
188
189    #[test]
190    fn instruction_data_has_expected_layout() {
191        let program_id = Pubkey::new_from_array([1; 32]);
192        let payer = Pubkey::new_from_array([2; 32]);
193        let state_pda = Pubkey::new_from_array([3; 32]);
194        let reclaim_authority = Pubkey::new_from_array([4; 32]);
195
196        let Instruction { data, .. } = Initialize {
197            program_id,
198            payer,
199            state_pda,
200            reclaim_authority,
201        }
202        .into();
203        assert_eq!(data.len(), EncodedStateAccount::SIZE);
204        assert_eq!(data[0], SettlementInstruction::Initialize.discriminator());
205        assert_eq!(&data[1..], &reclaim_authority.to_bytes());
206    }
207
208    #[test]
209    fn instruction_has_expected_accounts() {
210        let program_id = Pubkey::new_from_array([1; 32]);
211        let payer = Pubkey::new_from_array([2; 32]);
212        let state_pda = Pubkey::new_from_array([3; 32]);
213        let reclaim_authority = Pubkey::new_from_array([4; 32]);
214
215        let Instruction { accounts, .. } = Initialize {
216            program_id,
217            payer,
218            state_pda,
219            reclaim_authority,
220        }
221        .into();
222
223        assert_eq!(accounts.len(), 3);
224        // payer funds the new account's rent; state_pda is signed for by the
225        // program via PDA seeds; the system program is only referenced.
226        assert_writable_signer(&accounts[0], payer);
227        assert_writable_nonsigner(&accounts[1], state_pda);
228        assert_readonly_nonsigner(&accounts[2], SYSTEM_PROGRAM_ID);
229    }
230}