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 manager may authorize it. The state PDA
5//! grows by one solver, so a `payer` funds the extra rent through a `Transfer`
6//! 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/// `manager` authorizes the change and must be the state PDA's current manager;
22/// it signs but doesn't pay. `payer` funds the extra rent and signs the funding
23/// transfer. `solver` is inserted into the sorted solver list; adding one
24/// already present fails.
25///
26/// Wire format: `[discriminator=8, solver (32 bytes)]`.
27/// Required accounts: `[manager (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 manager: 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.manager, 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 manager: &'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: [manager (S), payer (W,S), state_pda (W), system_program (R)].
73        // The system program needs to be present for the `Transfer` CPI but
74        // doesn't need to be referenced directly and can be at any later position.
75        let [manager, payer, state_pda, _system, ..] = accounts else {
76            return Err(ProgramError::NotEnoughAccountKeys);
77        };
78
79        Ok(Self {
80            manager,
81            payer,
82            state_pda,
83            solver,
84        })
85    }
86}
87
88/// Test scaffolding for `AddSolver` parsing and handling, shared by this crate's
89/// tests and the settlement program's via the `test-fixtures` feature.
90#[cfg(any(test, feature = "test-fixtures"))]
91pub mod fixtures {
92    use solana_address::Address;
93    use solana_instruction::Instruction;
94
95    use super::AddSolver;
96
97    /// Number of accounts `AddSolver` expects: manager, payer, state PDA, system
98    /// program.
99    pub const NUM_ACCOUNTS: usize = 4;
100
101    /// `AddSolver` instruction data with placeholder addresses, for failure cases
102    /// where the actual addresses don't matter.
103    pub fn add_solver_data() -> Vec<u8> {
104        let zero = Address::default();
105        Instruction::from(AddSolver {
106            program_id: zero,
107            manager: zero,
108            payer: zero,
109            state_pda: zero,
110            solver: zero,
111        })
112        .data
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::fixtures::{add_solver_data, NUM_ACCOUNTS};
119    use super::*;
120    use crate::fixtures::pubkey_from_seed;
121    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
122    use crate::instruction::tests::{
123        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
124        assert_writable_signer,
125    };
126    use solana_account_view::AccountView;
127
128    #[test]
129    fn add_solver_input_parses_valid_input() {
130        let program_id = pubkey_from_seed("program id");
131        let manager = fake_account(pubkey_from_seed("manager"));
132        let payer = fake_account(pubkey_from_seed("payer"));
133        let state_pda = fake_account(pubkey_from_seed("state pda"));
134        let system_program = fake_account(pubkey_from_seed("system program"));
135        let solver = pubkey_from_seed("solver");
136
137        let data = Instruction::from(AddSolver {
138            program_id,
139            manager: *manager.address(),
140            payer: *payer.address(),
141            state_pda: *state_pda.address(),
142            solver,
143        })
144        .data;
145        let accounts = [manager, payer, state_pda, system_program];
146
147        let AddSolverInput {
148            manager: parsed_manager,
149            payer: parsed_payer,
150            state_pda: parsed_state_pda,
151            solver: parsed_solver,
152        } = AddSolverInput::parse(&data, &accounts).expect("parse should succeed");
153
154        assert_eq!(parsed_manager.address(), manager.address());
155        assert_eq!(parsed_payer.address(), payer.address());
156        assert_eq!(parsed_state_pda.address(), state_pda.address());
157        assert_eq!(parsed_solver, solver);
158    }
159
160    #[test]
161    fn add_solver_input_rejects_long_data() {
162        let mut data = add_solver_data();
163        data.push(42); // trailing byte
164        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
165        assert_eq!(
166            AddSolverInput::parse(&data, &accounts).err(),
167            Some(ProgramError::InvalidInstructionData),
168        );
169    }
170
171    #[test]
172    fn add_solver_input_rejects_short_data() {
173        let mut data = add_solver_data();
174        data.pop(); // one byte short
175        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
176        assert_eq!(
177            AddSolverInput::parse(&data, &accounts).err(),
178            Some(ProgramError::InvalidInstructionData),
179        );
180    }
181
182    #[test]
183    fn add_solver_input_rejects_missing_accounts() {
184        let data = add_solver_data();
185        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
186        accounts.pop();
187        assert_eq!(
188            AddSolverInput::parse(&data, &accounts).err(),
189            Some(ProgramError::NotEnoughAccountKeys),
190        );
191    }
192
193    #[test]
194    fn instruction_data_has_expected_layout() {
195        let solver = pubkey_from_seed("solver");
196        let Instruction { data, .. } = AddSolver {
197            program_id: pubkey_from_seed("program id"),
198            manager: pubkey_from_seed("manager"),
199            payer: pubkey_from_seed("payer"),
200            state_pda: pubkey_from_seed("state pda"),
201            solver,
202        }
203        .into();
204
205        assert_eq!(data.len(), 1 + size_of::<Pubkey>());
206        assert_eq!(data[0], SettlementInstruction::AddSolver.discriminator());
207        assert_eq!(&data[1..], &solver.to_bytes());
208    }
209
210    #[test]
211    fn instruction_data_regression() {
212        let solver = Pubkey::new_from_array([0x11; 32]);
213        let Instruction { data, .. } = AddSolver {
214            program_id: pubkey_from_seed("program id"),
215            manager: pubkey_from_seed("manager"),
216            payer: pubkey_from_seed("payer"),
217            state_pda: pubkey_from_seed("state pda"),
218            solver,
219        }
220        .into();
221
222        #[rustfmt::skip]
223        let expected: [u8; 1 + size_of::<Pubkey>()] = [
224            // discriminator (AddSolver = 8)
225            0x08,
226            // solver
227            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
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        ];
232        assert_eq!(data, expected);
233    }
234
235    #[test]
236    fn instruction_has_expected_accounts() {
237        let manager = pubkey_from_seed("manager");
238        let payer = pubkey_from_seed("payer");
239        let state_pda = pubkey_from_seed("state pda");
240        let Instruction { accounts, .. } = AddSolver {
241            program_id: pubkey_from_seed("program id"),
242            manager,
243            payer,
244            state_pda,
245            solver: pubkey_from_seed("solver"),
246        }
247        .into();
248
249        assert_eq!(accounts.len(), 4);
250        // The manager authorizes the change; the payer funds the extra rent; the
251        // state PDA is grown and written; the system program is only referenced.
252        assert_readonly_signer(&accounts[0], manager);
253        assert_writable_signer(&accounts[1], payer);
254        assert_writable_nonsigner(&accounts[2], state_pda);
255        assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
256    }
257}