Skip to main content

cow_settlement_interface/instruction/
create_order.rs

1//! `CreateOrder` instruction builder.
2//!
3//! Allocates a per-order PDA (see [`crate::pda::order`]) and writes the
4//! initial body bytes; the PDA's storage layout lives in
5//! [`crate::data::order`].
6
7use solana_instruction::{AccountMeta, Instruction};
8use solana_program_error::ProgramError;
9use solana_pubkey::Pubkey;
10
11pub use solana_system_interface::program::ID as SYSTEM_PROGRAM_ID;
12
13use super::InstructionInputParsing;
14use crate::{data::intent::EncodedOrderIntent, SettlementInstruction};
15
16/// Builder for a `CreateOrder` instruction.
17///
18/// `intent_bytes` is the canonical byte encoding (see
19/// [`EncodedOrderIntent`]). `order_pda` must be the canonical PDA returned
20/// by [`crate::pda::order::find_order_pda`] for the same UID; the program
21/// derives the bump itself and rejects any other address.
22///
23/// `owner` signs the instruction and must match the intent owner; this is
24/// what authenticates the order. The intent must also be flagged
25/// `created_on_chain`, the authentication scheme this instruction implements,
26/// or the program rejects it. It may be a normal user account or a PDA,
27/// the program does not check `is_on_curve`. A parent program that wants to
28/// create orders on behalf of its own PDA can `invoke_signed` into the
29/// settlement program using this instruction directly.
30///
31/// `created_by` funds the new order PDA's rent and is recorded as the
32/// `created_by` address in its body. Keeping it separate from `owner` lets
33/// the rent cost be delegated to another account, which is handy when
34/// `owner` is a PDA. `created_by` and `owner` may be the same account.
35///
36/// The order doesn't need to be executable for it to be created. For
37/// example, the sell token account doesn't need to belong to the user or
38/// be an SPL token account in the first place. This is checked at execution
39/// time.
40///
41/// An order can exist only once: if `order_pda` is already initialized, the
42/// instruction reverts with `AccountAlreadyInitialized`. Recreating the same
43/// order is only possible after its PDA has been closed.
44///
45/// Wire format: `[discriminator=2, ..intent bytes]`, 1 +
46/// [`EncodedOrderIntent::SIZE`]-bytes.
47/// Required accounts:
48/// `[owner (S), created_by (W,S), order_pda (W), system_program (R)]`.
49/// The system program needs to be available but doesn't need to be at that
50/// specific position in the instruction, unlike the others.
51pub struct CreateOrder {
52    pub program_id: Pubkey,
53    pub owner: Pubkey,
54    pub created_by: Pubkey,
55    pub order_pda: Pubkey,
56    pub intent_bytes: [u8; EncodedOrderIntent::SIZE],
57}
58
59impl From<CreateOrder> for Instruction {
60    fn from(builder: CreateOrder) -> Self {
61        let mut data = Vec::with_capacity(1 + EncodedOrderIntent::SIZE);
62        data.push(SettlementInstruction::CreateOrder.discriminator());
63        data.extend_from_slice(&builder.intent_bytes);
64
65        Instruction {
66            program_id: builder.program_id,
67            accounts: vec![
68                AccountMeta::new_readonly(builder.owner, true),
69                AccountMeta::new(builder.created_by, true),
70                AccountMeta::new(builder.order_pda, false),
71                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
72            ],
73            data,
74        }
75    }
76}
77
78/// Parsed inputs of a `CreateOrder` instruction.
79pub struct CreateOrderInput<'a, A> {
80    pub intent_bytes: [u8; EncodedOrderIntent::SIZE],
81    pub owner: &'a A,
82    pub created_by: &'a A,
83    pub order_pda: &'a A,
84}
85
86impl<'a, A> InstructionInputParsing<'a, A> for CreateOrderInput<'a, A> {
87    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::CreateOrder;
88
89    fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
90        // Body (discriminator already stripped): exactly EncodedOrderIntent::SIZE
91        // intent bytes.
92        if instruction_data.len() != EncodedOrderIntent::SIZE {
93            return Err(ProgramError::InvalidInstructionData);
94        }
95        // Accounts: [owner (S), created_by (W,S), order_pda (W), some other
96        // account]. We check that there are four accounts because the
97        // instruction needs to specify `SYSTEM_PROGRAM_ID` as one of the
98        // signers. It doesn't have to be the fourth though.
99        let [owner, created_by, order_pda, _, ..] = accounts else {
100            return Err(ProgramError::NotEnoughAccountKeys);
101        };
102
103        let intent_bytes: [u8; EncodedOrderIntent::SIZE] =
104            instruction_data.try_into().expect("length checked above");
105
106        Ok(Self {
107            intent_bytes,
108            owner,
109            created_by,
110            order_pda,
111        })
112    }
113}
114
115/// Test scaffolding for `CreateOrder` parsing and handling, shared by this
116/// crate's tests and the settlement program's via the `test-fixtures` feature.
117#[cfg(any(test, feature = "test-fixtures"))]
118pub mod fixtures {
119    use solana_address::Address;
120
121    use super::{CreateOrder, Instruction};
122    use crate::data::intent::{fixtures::sample_intent, EncodedOrderIntent};
123
124    /// Number of accounts `CreateOrder` expects: owner, created_by, order PDA,
125    /// and the system program.
126    pub const NUM_ACCOUNTS: usize = 4;
127
128    /// Canonical [`EncodedOrderIntent::SIZE`]-byte intent payload for a valid
129    /// sell order. The owner is [`sample_intent`]'s; tests that need it read it
130    /// back from the decoded intent rather than assuming a value.
131    pub fn valid_intent_bytes() -> [u8; EncodedOrderIntent::SIZE] {
132        (&EncodedOrderIntent::from(&sample_intent(Default::default()))).into()
133    }
134
135    /// `CreateOrder` instruction data carrying `intent_bytes`, with placeholder
136    /// addresses for failure cases where the actual addresses don't matter.
137    pub fn default_order_data(intent_bytes: &[u8; EncodedOrderIntent::SIZE]) -> Vec<u8> {
138        let zero = Address::new_from_array([0; 32]);
139        Instruction::from(CreateOrder {
140            program_id: zero,
141            owner: zero,
142            created_by: zero,
143            order_pda: zero,
144            intent_bytes: *intent_bytes,
145        })
146        .data
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::fixtures::{default_order_data, valid_intent_bytes, NUM_ACCOUNTS};
153    use super::*;
154    use crate::instruction::fixtures::{
155        fake_account, fake_account_from_array, fake_sequential_accounts,
156    };
157    use crate::instruction::tests::{
158        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
159        assert_writable_signer,
160    };
161    use solana_account_view::AccountView;
162    use solana_address::Address;
163
164    #[test]
165    fn create_order_input_parses_valid_input() {
166        let program_id = Address::new_from_array([21; 32]);
167        let owner = Address::new_from_array([22; 32]);
168        let created_by = Address::new_from_array([24; 32]);
169        let order_pda = Address::new_from_array([23; 32]);
170        let intent_bytes = valid_intent_bytes();
171
172        let data = Instruction::from(CreateOrder {
173            program_id,
174            owner,
175            created_by,
176            order_pda,
177            intent_bytes,
178        })
179        .data;
180        let accounts = [
181            fake_account(owner),
182            fake_account(created_by),
183            fake_account(order_pda),
184            fake_account_from_array([4; 32]),
185        ];
186
187        let CreateOrderInput {
188            intent_bytes: derived_intent_bytes,
189            owner: derived_owner,
190            created_by: derived_created_by,
191            order_pda: derived_order_pda,
192        } = CreateOrderInput::parse(&data, &accounts).expect("parse should succeed");
193
194        assert_eq!(derived_intent_bytes, intent_bytes);
195        assert_eq!(*derived_order_pda.address(), order_pda);
196        assert_eq!(*derived_owner.address(), owner);
197        assert_eq!(*derived_created_by.address(), created_by);
198    }
199
200    #[test]
201    fn create_order_input_rejects_short_data() {
202        let intent_bytes = valid_intent_bytes();
203        let mut data = default_order_data(&intent_bytes);
204        data.pop();
205        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
206        assert_eq!(
207            CreateOrderInput::parse(&data, &accounts).err(),
208            Some(ProgramError::InvalidInstructionData),
209        );
210    }
211
212    #[test]
213    fn create_order_input_rejects_long_data() {
214        let intent_bytes = valid_intent_bytes();
215        let mut data = default_order_data(&intent_bytes);
216        data.push(0); // trailing byte
217        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
218        assert_eq!(
219            CreateOrderInput::parse(&data, &accounts).err(),
220            Some(ProgramError::InvalidInstructionData),
221        );
222    }
223
224    #[test]
225    fn create_order_input_rejects_missing_accounts() {
226        let intent_bytes = valid_intent_bytes();
227        let data = default_order_data(&intent_bytes);
228        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
229        accounts.pop();
230        assert_eq!(
231            CreateOrderInput::parse(&data, &accounts).err(),
232            Some(ProgramError::NotEnoughAccountKeys),
233        );
234    }
235
236    #[test]
237    fn instruction_data_has_expected_layout() {
238        let program_id = Pubkey::new_from_array([1; 32]);
239        let owner = Pubkey::new_from_array([2; 32]);
240        let created_by = Pubkey::new_from_array([4; 32]);
241        let order_pda = Pubkey::new_from_array([3; 32]);
242        let intent_bytes = [0x42u8; EncodedOrderIntent::SIZE];
243
244        let Instruction { data, .. } = CreateOrder {
245            program_id,
246            owner,
247            created_by,
248            order_pda,
249            intent_bytes,
250        }
251        .into();
252
253        assert_eq!(data.len(), 1 + EncodedOrderIntent::SIZE);
254        assert_eq!(data[0], SettlementInstruction::CreateOrder.discriminator());
255        assert_eq!(&data[1..], &intent_bytes);
256    }
257
258    #[test]
259    fn instruction_data_has_expected_accounts() {
260        let program_id = Pubkey::new_from_array([1; 32]);
261        let owner = Pubkey::new_from_array([2; 32]);
262        let created_by = Pubkey::new_from_array([4; 32]);
263        let order_pda = Pubkey::new_from_array([3; 32]);
264        let intent_bytes = [0u8; EncodedOrderIntent::SIZE];
265
266        let Instruction { accounts, .. } = CreateOrder {
267            program_id,
268            owner,
269            created_by,
270            order_pda,
271            intent_bytes,
272        }
273        .into();
274
275        assert_eq!(accounts.len(), 4);
276        // owner authenticates the order without paying rent; created_by funds
277        // the new PDA's rent; order_pda is created. The system program is
278        // only referenced.
279        assert_readonly_signer(&accounts[0], owner);
280        assert_writable_signer(&accounts[1], created_by);
281        assert_writable_nonsigner(&accounts[2], order_pda);
282        assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
283    }
284}