cow-settlement-interface 0.5.1

Account layouts, PDA derivations, and instruction builders for the CoW Protocol Solana settlement program.
Documentation
//! `AddSolver` instruction builder and parser.
//!
//! It inserts a solver into the sorted solver list stored in the state PDA (see
//! [`crate::data::state`]). Only the solver authority may authorize it. The
//! state PDA grows by one solver, so a `payer` funds the extra rent through a
//! `Transfer` from the system program.

use core::mem::size_of;

use solana_instruction::{AccountMeta, Instruction};
use solana_program_error::ProgramError;
use solana_pubkey::Pubkey;

pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;

use crate::instruction::InstructionInputParsing;
use crate::SettlementInstruction;

/// Builder for an `AddSolver` instruction.
///
/// `authority` authorizes the change and must be the state PDA's current solver
/// authority; it signs but doesn't pay. `payer` funds the extra rent and signs
/// the funding transfer. `solver` is inserted into the sorted solver list;
/// adding one already present fails.
///
/// Wire format: `[discriminator=8, solver (32 bytes)]`.
/// Required accounts: `[authority (S), payer (W,S), state_pda (W),
/// system_program (R)]`. The system program must be available for the
/// rent-funding `Transfer` CPI but doesn't need to sit at a specific position.
pub struct AddSolver {
    pub program_id: Pubkey,
    pub authority: Pubkey,
    pub payer: Pubkey,
    pub state_pda: Pubkey,
    pub solver: Pubkey,
}

impl From<AddSolver> for Instruction {
    fn from(builder: AddSolver) -> Self {
        let mut data = vec![SettlementInstruction::AddSolver.discriminator()];
        data.extend_from_slice(&builder.solver.to_bytes());
        Instruction {
            program_id: builder.program_id,
            accounts: vec![
                AccountMeta::new_readonly(builder.authority, true),
                AccountMeta::new(builder.payer, true),
                AccountMeta::new(builder.state_pda, false),
                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
            ],
            data,
        }
    }
}

/// Parsed inputs of an `AddSolver` instruction.
pub struct AddSolverInput<'a, A> {
    pub authority: &'a A,
    pub payer: &'a A,
    pub state_pda: &'a A,
    pub solver: Pubkey,
}

impl<'a, A> InstructionInputParsing<'a, A> for AddSolverInput<'a, A> {
    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::AddSolver;

    fn parse_body(instruction_data: &[u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
        let solver: &[u8; size_of::<Pubkey>()] = instruction_data
            .try_into()
            .map_err(|_| ProgramError::InvalidInstructionData)?;
        let solver = Pubkey::new_from_array(*solver);

        // Accounts: [authority (S), payer (W,S), state_pda (W),
        // system_program (R)]. The system program needs to be present for the
        // `Transfer` CPI but doesn't need to be referenced directly and can be
        // at any later position.
        let [authority, payer, state_pda, _system, ..] = accounts else {
            return Err(ProgramError::NotEnoughAccountKeys);
        };

        Ok(Self {
            authority,
            payer,
            state_pda,
            solver,
        })
    }
}

/// Test scaffolding for `AddSolver` parsing and handling, shared by this crate's
/// tests and the settlement program's via the `test-fixtures` feature.
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
    use solana_address::Address;
    use solana_instruction::Instruction;

    use super::AddSolver;

    /// Number of accounts `AddSolver` expects: solver authority, payer, state
    /// PDA, system program.
    pub const NUM_ACCOUNTS: usize = 4;

    /// `AddSolver` instruction data with placeholder addresses, for failure cases
    /// where the actual addresses don't matter.
    pub fn add_solver_data() -> Vec<u8> {
        let zero = Address::default();
        Instruction::from(AddSolver {
            program_id: zero,
            authority: zero,
            payer: zero,
            state_pda: zero,
            solver: zero,
        })
        .data
    }
}

#[cfg(test)]
mod tests {
    use super::fixtures::{add_solver_data, NUM_ACCOUNTS};
    use super::*;
    use crate::fixtures::pubkey_from_seed;
    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
    use crate::instruction::tests::{
        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
        assert_writable_signer,
    };
    use solana_account_view::AccountView;

    #[test]
    fn add_solver_input_parses_valid_input() {
        let program_id = pubkey_from_seed("program id");
        let authority = fake_account(pubkey_from_seed("solver authority"));
        let payer = fake_account(pubkey_from_seed("payer"));
        let state_pda = fake_account(pubkey_from_seed("state pda"));
        let system_program = fake_account(pubkey_from_seed("system program"));
        let solver = pubkey_from_seed("solver");

        let data = Instruction::from(AddSolver {
            program_id,
            authority: *authority.address(),
            payer: *payer.address(),
            state_pda: *state_pda.address(),
            solver,
        })
        .data;
        let accounts = [authority, payer, state_pda, system_program];

        let AddSolverInput {
            authority: parsed_authority,
            payer: parsed_payer,
            state_pda: parsed_state_pda,
            solver: parsed_solver,
        } = AddSolverInput::parse(&data, &accounts).expect("parse should succeed");

        assert_eq!(parsed_authority.address(), authority.address());
        assert_eq!(parsed_payer.address(), payer.address());
        assert_eq!(parsed_state_pda.address(), state_pda.address());
        assert_eq!(parsed_solver, solver);
    }

    #[test]
    fn add_solver_input_rejects_long_data() {
        let mut data = add_solver_data();
        data.push(42); // trailing byte
        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
        assert_eq!(
            AddSolverInput::parse(&data, &accounts).err(),
            Some(ProgramError::InvalidInstructionData),
        );
    }

    #[test]
    fn add_solver_input_rejects_short_data() {
        let mut data = add_solver_data();
        data.pop(); // one byte short
        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
        assert_eq!(
            AddSolverInput::parse(&data, &accounts).err(),
            Some(ProgramError::InvalidInstructionData),
        );
    }

    #[test]
    fn add_solver_input_rejects_missing_accounts() {
        let data = add_solver_data();
        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
        accounts.pop();
        assert_eq!(
            AddSolverInput::parse(&data, &accounts).err(),
            Some(ProgramError::NotEnoughAccountKeys),
        );
    }

    #[test]
    fn instruction_data_has_expected_layout() {
        let solver = pubkey_from_seed("solver");
        let Instruction { data, .. } = AddSolver {
            program_id: pubkey_from_seed("program id"),
            authority: pubkey_from_seed("solver authority"),
            payer: pubkey_from_seed("payer"),
            state_pda: pubkey_from_seed("state pda"),
            solver,
        }
        .into();

        assert_eq!(data.len(), 1 + size_of::<Pubkey>());
        assert_eq!(data[0], SettlementInstruction::AddSolver.discriminator());
        assert_eq!(&data[1..], &solver.to_bytes());
    }

    #[test]
    fn instruction_data_regression() {
        let solver = Pubkey::new_from_array([0x11; 32]);
        let Instruction { data, .. } = AddSolver {
            program_id: pubkey_from_seed("program id"),
            authority: pubkey_from_seed("solver authority"),
            payer: pubkey_from_seed("payer"),
            state_pda: pubkey_from_seed("state pda"),
            solver,
        }
        .into();

        #[rustfmt::skip]
        let expected: [u8; 1 + size_of::<Pubkey>()] = [
            // discriminator (AddSolver = 8)
            0x08,
            // solver
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
        ];
        assert_eq!(data, expected);
    }

    #[test]
    fn instruction_has_expected_accounts() {
        let authority = pubkey_from_seed("solver authority");
        let payer = pubkey_from_seed("payer");
        let state_pda = pubkey_from_seed("state pda");
        let Instruction { accounts, .. } = AddSolver {
            program_id: pubkey_from_seed("program id"),
            authority,
            payer,
            state_pda,
            solver: pubkey_from_seed("solver"),
        }
        .into();

        assert_eq!(accounts.len(), 4);
        // The solver authority authorizes the change; the payer funds the extra
        // rent; the state PDA is grown and written; the system program is only
        // referenced.
        assert_readonly_signer(&accounts[0], authority);
        assert_writable_signer(&accounts[1], payer);
        assert_writable_nonsigner(&accounts[2], state_pda);
        assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
    }
}