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