Skip to main content

cow_settlement_interface/instruction/
add_solver.rs

1//! `AddSolver` instruction builder and parser.
2//!
3//! It inserts a solver into the sorted solver list stored in the state PDA (see
4//! [`crate::data::state`]). Only the solver authority may authorize it. The
5//! state PDA grows by one solver, so a `payer` funds the extra rent through a
6//! `Transfer` from the system program.
7
8use core::mem::size_of;
9
10use solana_instruction::{AccountMeta, Instruction};
11use solana_program_error::ProgramError;
12use solana_pubkey::Pubkey;
13
14pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
15
16use crate::instruction::InstructionInputParsing;
17use crate::SettlementInstruction;
18
19/// Builder for an `AddSolver` instruction.
20///
21/// `authority` authorizes the change and must be the state PDA's current solver
22/// authority; it signs but doesn't pay. `payer` funds the extra rent and signs
23/// the funding transfer. `solver` is inserted into the sorted solver list;
24/// adding one already present fails.
25///
26/// Wire format: `[discriminator=8, solver (32 bytes)]`.
27/// Required accounts: `[authority (S), payer (W,S), state_pda (W),
28/// system_program (R)]`. The system program must be available for the
29/// rent-funding `Transfer` CPI but doesn't need to sit at a specific position.
30pub struct AddSolver {
31    pub program_id: Pubkey,
32    pub authority: Pubkey,
33    pub payer: Pubkey,
34    pub state_pda: Pubkey,
35    pub solver: Pubkey,
36}
37
38impl From<AddSolver> for Instruction {
39    fn from(builder: AddSolver) -> Self {
40        let mut data = vec![SettlementInstruction::AddSolver.discriminator()];
41        data.extend_from_slice(&builder.solver.to_bytes());
42        Instruction {
43            program_id: builder.program_id,
44            accounts: vec![
45                AccountMeta::new_readonly(builder.authority, true),
46                AccountMeta::new(builder.payer, true),
47                AccountMeta::new(builder.state_pda, false),
48                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
49            ],
50            data,
51        }
52    }
53}
54
55/// Parsed inputs of an `AddSolver` instruction.
56pub struct AddSolverInput<'a, A> {
57    pub authority: &'a A,
58    pub payer: &'a A,
59    pub state_pda: &'a A,
60    pub solver: Pubkey,
61}
62
63impl<'a, A> InstructionInputParsing<'a, A> for AddSolverInput<'a, A> {
64    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::AddSolver;
65
66    fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
67        let solver: &[u8; size_of::<Pubkey>()] = instruction_data
68            .try_into()
69            .map_err(|_| ProgramError::InvalidInstructionData)?;
70        let solver = Pubkey::new_from_array(*solver);
71
72        // Accounts: [authority (S), payer (W,S), state_pda (W),
73        // system_program (R)]. The system program needs to be present for the
74        // `Transfer` CPI but doesn't need to be referenced directly and can be
75        // at any later position.
76        let [authority, payer, state_pda, _system, ..] = accounts else {
77            return Err(ProgramError::NotEnoughAccountKeys);
78        };
79
80        Ok(Self {
81            authority,
82            payer,
83            state_pda,
84            solver,
85        })
86    }
87}
88
89/// Test scaffolding for `AddSolver` parsing and handling, shared by this crate's
90/// tests and the settlement program's via the `test-fixtures` feature.
91#[cfg(any(test, feature = "test-fixtures"))]
92pub mod fixtures {
93    use solana_address::Address;
94    use solana_instruction::Instruction;
95
96    use super::AddSolver;
97
98    /// Number of accounts `AddSolver` expects: solver authority, payer, state
99    /// PDA, system program.
100    pub const NUM_ACCOUNTS: usize = 4;
101
102    /// `AddSolver` instruction data with placeholder addresses, for failure cases
103    /// where the actual addresses don't matter.
104    pub fn add_solver_data() -> Vec<u8> {
105        let zero = Address::default();
106        Instruction::from(AddSolver {
107            program_id: zero,
108            authority: zero,
109            payer: zero,
110            state_pda: zero,
111            solver: zero,
112        })
113        .data
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::fixtures::{add_solver_data, NUM_ACCOUNTS};
120    use super::*;
121    use crate::fixtures::pubkey_from_seed;
122    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
123    use crate::instruction::tests::{
124        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
125        assert_writable_signer,
126    };
127    use solana_account_view::AccountView;
128
129    #[test]
130    fn add_solver_input_parses_valid_input() {
131        let program_id = pubkey_from_seed("program id");
132        let authority = fake_account(pubkey_from_seed("solver authority"));
133        let payer = fake_account(pubkey_from_seed("payer"));
134        let state_pda = fake_account(pubkey_from_seed("state pda"));
135        let system_program = fake_account(pubkey_from_seed("system program"));
136        let solver = pubkey_from_seed("solver");
137
138        let data = Instruction::from(AddSolver {
139            program_id,
140            authority: *authority.address(),
141            payer: *payer.address(),
142            state_pda: *state_pda.address(),
143            solver,
144        })
145        .data;
146        let accounts = [authority, payer, state_pda, system_program];
147
148        let AddSolverInput {
149            authority: parsed_authority,
150            payer: parsed_payer,
151            state_pda: parsed_state_pda,
152            solver: parsed_solver,
153        } = AddSolverInput::parse(&data, &accounts).expect("parse should succeed");
154
155        assert_eq!(parsed_authority.address(), authority.address());
156        assert_eq!(parsed_payer.address(), payer.address());
157        assert_eq!(parsed_state_pda.address(), state_pda.address());
158        assert_eq!(parsed_solver, solver);
159    }
160
161    #[test]
162    fn add_solver_input_rejects_long_data() {
163        let mut data = add_solver_data();
164        data.push(42); // trailing byte
165        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
166        assert_eq!(
167            AddSolverInput::parse(&data, &accounts).err(),
168            Some(ProgramError::InvalidInstructionData),
169        );
170    }
171
172    #[test]
173    fn add_solver_input_rejects_short_data() {
174        let mut data = add_solver_data();
175        data.pop(); // one byte short
176        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
177        assert_eq!(
178            AddSolverInput::parse(&data, &accounts).err(),
179            Some(ProgramError::InvalidInstructionData),
180        );
181    }
182
183    #[test]
184    fn add_solver_input_rejects_missing_accounts() {
185        let data = add_solver_data();
186        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
187        accounts.pop();
188        assert_eq!(
189            AddSolverInput::parse(&data, &accounts).err(),
190            Some(ProgramError::NotEnoughAccountKeys),
191        );
192    }
193
194    #[test]
195    fn instruction_data_has_expected_layout() {
196        let solver = pubkey_from_seed("solver");
197        let Instruction { data, .. } = AddSolver {
198            program_id: pubkey_from_seed("program id"),
199            authority: pubkey_from_seed("solver authority"),
200            payer: pubkey_from_seed("payer"),
201            state_pda: pubkey_from_seed("state pda"),
202            solver,
203        }
204        .into();
205
206        assert_eq!(data.len(), 1 + size_of::<Pubkey>());
207        assert_eq!(data[0], SettlementInstruction::AddSolver.discriminator());
208        assert_eq!(&data[1..], &solver.to_bytes());
209    }
210
211    #[test]
212    fn instruction_data_regression() {
213        let solver = Pubkey::new_from_array([0x11; 32]);
214        let Instruction { data, .. } = AddSolver {
215            program_id: pubkey_from_seed("program id"),
216            authority: pubkey_from_seed("solver authority"),
217            payer: pubkey_from_seed("payer"),
218            state_pda: pubkey_from_seed("state pda"),
219            solver,
220        }
221        .into();
222
223        #[rustfmt::skip]
224        let expected: [u8; 1 + size_of::<Pubkey>()] = [
225            // discriminator (AddSolver = 8)
226            0x08,
227            // solver
228            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
229            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
230            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
231            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
232        ];
233        assert_eq!(data, expected);
234    }
235
236    #[test]
237    fn instruction_has_expected_accounts() {
238        let authority = pubkey_from_seed("solver authority");
239        let payer = pubkey_from_seed("payer");
240        let state_pda = pubkey_from_seed("state pda");
241        let Instruction { accounts, .. } = AddSolver {
242            program_id: pubkey_from_seed("program id"),
243            authority,
244            payer,
245            state_pda,
246            solver: pubkey_from_seed("solver"),
247        }
248        .into();
249
250        assert_eq!(accounts.len(), 4);
251        // The solver authority authorizes the change; the payer funds the extra
252        // rent; the state PDA is grown and written; the system program is only
253        // referenced.
254        assert_readonly_signer(&accounts[0], authority);
255        assert_writable_signer(&accounts[1], payer);
256        assert_writable_nonsigner(&accounts[2], state_pda);
257        assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
258    }
259}