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 super::InstructionInputParsing;
use crate::{data::intent::EncodedOrderIntent, SettlementInstruction};
pub struct CancelOrder {
pub program_id: Pubkey,
pub owner: Pubkey,
pub created_by: Pubkey,
pub order_pda: Pubkey,
pub intent_bytes: Option<[u8; EncodedOrderIntent::SIZE]>,
}
impl From<CancelOrder> for Instruction {
fn from(builder: CancelOrder) -> Self {
let mut data = Vec::with_capacity(1 + EncodedOrderIntent::SIZE);
data.push(SettlementInstruction::CancelOrder.discriminator());
let created_by = match builder.intent_bytes {
Some(intent_bytes) => {
data.extend_from_slice(&intent_bytes);
AccountMeta::new(builder.created_by, true)
}
None => AccountMeta::new_readonly(builder.created_by, false),
};
Instruction {
program_id: builder.program_id,
accounts: vec![
AccountMeta::new_readonly(builder.owner, true),
created_by,
AccountMeta::new(builder.order_pda, false),
AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
],
data,
}
}
}
pub struct CancelOrderInput<'a, A> {
pub intent_bytes: Option<[u8; EncodedOrderIntent::SIZE]>,
pub owner: &'a A,
pub created_by: &'a A,
pub order_pda: &'a A,
}
impl<'a, A> InstructionInputParsing<'a, A> for CancelOrderInput<'a, A> {
const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::CancelOrder;
fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
let intent_bytes = if instruction_data.is_empty() {
None
} else {
Some(
instruction_data
.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?,
)
};
let [owner, created_by, order_pda, _, ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
Ok(Self {
intent_bytes,
owner,
created_by,
order_pda,
})
}
}
#[cfg(any(test, feature = "test-fixtures"))]
pub mod fixtures {
use solana_address::Address;
use super::{CancelOrder, Instruction};
use crate::data::intent::EncodedOrderIntent;
pub use crate::instruction::create_order::fixtures::valid_intent_bytes;
pub const NUM_ACCOUNTS: usize = 4;
pub fn default_cancel_data(intent_bytes: Option<[u8; EncodedOrderIntent::SIZE]>) -> Vec<u8> {
let zero = Address::new_from_array([0; 32]);
Instruction::from(CancelOrder {
program_id: zero,
owner: zero,
created_by: zero,
order_pda: zero,
intent_bytes,
})
.data
}
}
#[cfg(test)]
mod tests {
use super::fixtures::{default_cancel_data, valid_intent_bytes, 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 cancel_order_input_parses_valid_input() {
let program_id = pubkey_from_seed("program id");
let owner = pubkey_from_seed("owner");
let created_by = pubkey_from_seed("created by");
let order_pda = pubkey_from_seed("order pda");
let intent_bytes = valid_intent_bytes();
let data = Instruction::from(CancelOrder {
program_id,
owner,
created_by,
order_pda,
intent_bytes: Some(intent_bytes),
})
.data;
let accounts = [
fake_account(owner),
fake_account(created_by),
fake_account(order_pda),
fake_account(pubkey_from_seed("system program")),
];
let CancelOrderInput {
intent_bytes: derived_intent_bytes,
owner: derived_owner,
created_by: derived_created_by,
order_pda: derived_order_pda,
} = CancelOrderInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(derived_intent_bytes, Some(intent_bytes));
assert_eq!(*derived_order_pda.address(), order_pda);
assert_eq!(*derived_owner.address(), owner);
assert_eq!(*derived_created_by.address(), created_by);
}
#[test]
fn cancel_order_input_parses_without_intent() {
let owner = pubkey_from_seed("owner");
let created_by = pubkey_from_seed("created by");
let order_pda = pubkey_from_seed("order pda");
let data = default_cancel_data(None);
let accounts = [
fake_account(owner),
fake_account(created_by),
fake_account(order_pda),
fake_account(pubkey_from_seed("system program")),
];
let CancelOrderInput {
intent_bytes: derived_intent_bytes,
owner: derived_owner,
order_pda: derived_order_pda,
..
} = CancelOrderInput::parse(&data, &accounts).expect("parse should succeed");
assert_eq!(
derived_intent_bytes, None,
"an empty body carries no intent"
);
assert_eq!(*derived_order_pda.address(), order_pda);
assert_eq!(*derived_owner.address(), owner);
}
#[test]
fn cancel_order_input_rejects_short_data() {
let intent_bytes = valid_intent_bytes();
let mut data = default_cancel_data(Some(intent_bytes));
data.pop();
let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
assert_eq!(
CancelOrderInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn cancel_order_input_rejects_long_data() {
let intent_bytes = valid_intent_bytes();
let mut data = default_cancel_data(Some(intent_bytes));
data.push(0); let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
assert_eq!(
CancelOrderInput::parse(&data, &accounts).err(),
Some(ProgramError::InvalidInstructionData),
);
}
#[test]
fn cancel_order_input_rejects_missing_accounts() {
let intent_bytes = valid_intent_bytes();
let data = default_cancel_data(Some(intent_bytes));
let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
accounts.pop();
assert_eq!(
CancelOrderInput::parse(&data, &accounts).err(),
Some(ProgramError::NotEnoughAccountKeys),
);
}
#[test]
fn instruction_data_has_expected_layout() {
let program_id = pubkey_from_seed("program id");
let owner = pubkey_from_seed("owner");
let created_by = pubkey_from_seed("created by");
let order_pda = pubkey_from_seed("order pda");
let intent_bytes = [0x42u8; EncodedOrderIntent::SIZE];
let Instruction { data, .. } = CancelOrder {
program_id,
owner,
created_by,
order_pda,
intent_bytes: Some(intent_bytes),
}
.into();
assert_eq!(data.len(), 1 + EncodedOrderIntent::SIZE);
assert_eq!(data[0], SettlementInstruction::CancelOrder.discriminator());
assert_eq!(&data[1..], &intent_bytes);
}
#[test]
fn instruction_data_without_intent_is_discriminator_only() {
let Instruction { data, .. } = CancelOrder {
program_id: pubkey_from_seed("program id"),
owner: pubkey_from_seed("owner"),
created_by: pubkey_from_seed("created by"),
order_pda: pubkey_from_seed("order pda"),
intent_bytes: None,
}
.into();
assert_eq!(
data,
vec![SettlementInstruction::CancelOrder.discriminator()],
"an intent-less cancellation carries only the discriminator"
);
}
#[test]
fn instruction_accounts_without_intent_leave_created_by_unsigned() {
let program_id = pubkey_from_seed("program id");
let owner = pubkey_from_seed("owner");
let created_by = pubkey_from_seed("created by");
let order_pda = pubkey_from_seed("order pda");
let Instruction { accounts, .. } = CancelOrder {
program_id,
owner,
created_by,
order_pda,
intent_bytes: None,
}
.into();
assert_eq!(accounts.len(), 4);
assert_readonly_signer(&accounts[0], owner);
assert_readonly_nonsigner(&accounts[1], created_by);
assert_writable_nonsigner(&accounts[2], order_pda);
assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
}
#[test]
fn instruction_data_has_expected_accounts() {
let program_id = pubkey_from_seed("program id");
let owner = pubkey_from_seed("owner");
let created_by = pubkey_from_seed("created by");
let order_pda = pubkey_from_seed("order pda");
let intent_bytes = [0u8; EncodedOrderIntent::SIZE];
let Instruction { accounts, .. } = CancelOrder {
program_id,
owner,
created_by,
order_pda,
intent_bytes: Some(intent_bytes),
}
.into();
assert_eq!(accounts.len(), 4);
assert_readonly_signer(&accounts[0], owner);
assert_writable_signer(&accounts[1], created_by);
assert_writable_nonsigner(&accounts[2], order_pda);
assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
}
}