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