Skip to main content

cow_settlement_interface/instruction/
cancel_order.rs

1//! `CancelOrder` instruction builder.
2//!
3//! Cancels the order for a given intent by setting the `cancelled` flag on its
4//! PDA (see [`crate::data::order::OrderAccount`]); the PDA is created already
5//! cancelled if it doesn't exist yet.
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 `CancelOrder` instruction.
17///
18/// `owner` signs the instruction and must match the intent owner; only the
19/// owner can cancel their order.
20///
21/// `intent_bytes` is optional. When present, it's the encoded intent (the same
22/// input as [`super::create_order::CreateOrder`]) and lets this instruction
23/// create the order PDA already cancelled if it doesn't exist yet. When absent,
24/// the order must already exist: its data is recovered from the `order_pda`
25/// account and only its `cancelled` flag is set.
26///
27/// `created_by` funds the order PDA's rent on the create path, and must sign to
28/// authorize that rent. When the order already exists no rent moves and
29/// `created_by` is unused, so it doesn't need to sign (and doesn't need to be
30/// meaningful when `intent_bytes` is absent).
31///
32/// Cancelling is idempotent: cancelling an already-cancelled order does
33/// nothing.
34///
35/// The rent of a cancelled order can be recovered afterwards with
36/// [`super::reclaim_order`], which reclaims a `created_on_chain` order early
37/// once it is cancelled.
38///
39/// Wire format: `[discriminator=11]` to cancel an existing order, or
40/// `[discriminator=11, ..intent bytes]` (1 + [`EncodedOrderIntent::SIZE`]
41/// bytes) to also create it already cancelled when it's missing.
42/// Required accounts:
43/// `[owner (S), created_by (W,S), order_pda (W), system_program (R)]`.
44/// When the PDA already exists, `created_by` neither signs nor is written and
45/// the system program is unused, but the accounts must still occupy these
46/// slots. The system program needs to be available but doesn't need to be at
47/// that specific position in the instruction, unlike the others.
48pub struct CancelOrder {
49    pub program_id: Pubkey,
50    pub owner: Pubkey,
51    pub created_by: Pubkey,
52    pub order_pda: Pubkey,
53    pub intent_bytes: Option<[u8; EncodedOrderIntent::SIZE]>,
54}
55
56impl From<CancelOrder> for Instruction {
57    fn from(builder: CancelOrder) -> Self {
58        let mut data = Vec::with_capacity(1 + EncodedOrderIntent::SIZE);
59        data.push(SettlementInstruction::CancelOrder.discriminator());
60
61        // `created_by` only funds rent on the create path, so it signs (and is
62        // written) only when the intent is supplied; recovering an existing
63        // order touches neither the funder nor the system program.
64        let created_by = match builder.intent_bytes {
65            Some(intent_bytes) => {
66                data.extend_from_slice(&intent_bytes);
67                AccountMeta::new(builder.created_by, true)
68            }
69            None => AccountMeta::new_readonly(builder.created_by, false),
70        };
71
72        Instruction {
73            program_id: builder.program_id,
74            accounts: vec![
75                AccountMeta::new_readonly(builder.owner, true),
76                created_by,
77                AccountMeta::new(builder.order_pda, false),
78                AccountMeta::new_readonly(SYSTEM_PROGRAM_ID, false),
79            ],
80            data,
81        }
82    }
83}
84
85/// Parsed inputs of a `CancelOrder` instruction. Mirrors
86/// [`super::create_order::CreateOrderInput`], except the intent is optional:
87/// it's absent when the caller cancels an order that already exists on-chain.
88pub struct CancelOrderInput<'a, A> {
89    pub intent_bytes: Option<[u8; EncodedOrderIntent::SIZE]>,
90    pub owner: &'a A,
91    pub created_by: &'a A,
92    pub order_pda: &'a A,
93}
94
95impl<'a, A> InstructionInputParsing<'a, A> for CancelOrderInput<'a, A> {
96    const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::CancelOrder;
97
98    fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
99        let intent_bytes = if instruction_data.is_empty() {
100            None
101        } else {
102            Some(
103                instruction_data
104                    .try_into()
105                    .map_err(|_| ProgramError::InvalidInstructionData)?,
106            )
107        };
108
109        // Accounts: [owner (S), created_by (W,S), order_pda (W), some other
110        // account]. We check that there are four accounts because the
111        // instruction needs to specify `SYSTEM_PROGRAM_ID` as one of the
112        // signers. It doesn't have to be the fourth though.
113        let [owner, created_by, order_pda, _, ..] = accounts else {
114            return Err(ProgramError::NotEnoughAccountKeys);
115        };
116
117        Ok(Self {
118            intent_bytes,
119            owner,
120            created_by,
121            order_pda,
122        })
123    }
124}
125
126/// Test scaffolding for `CancelOrder` parsing and handling, shared by this
127/// crate's tests and the settlement program's via the `test-fixtures` feature.
128#[cfg(any(test, feature = "test-fixtures"))]
129pub mod fixtures {
130    use solana_address::Address;
131
132    use super::{CancelOrder, Instruction};
133    use crate::data::intent::EncodedOrderIntent;
134
135    // `CancelOrder` takes the same intent input as `CreateOrder`, so reuse its
136    // sample payload rather than duplicating it.
137    pub use crate::instruction::create_order::fixtures::valid_intent_bytes;
138
139    /// Number of accounts `CancelOrder` expects: owner, created_by, order PDA,
140    /// and the system program.
141    pub const NUM_ACCOUNTS: usize = 4;
142
143    /// `CancelOrder` instruction data with placeholder addresses, for failure
144    /// cases where the actual addresses don't matter. `intent_bytes` is `Some`
145    /// for the create-cancelled form and `None` for the recover-from-PDA form.
146    pub fn default_cancel_data(intent_bytes: Option<[u8; EncodedOrderIntent::SIZE]>) -> Vec<u8> {
147        let zero = Address::new_from_array([0; 32]);
148        Instruction::from(CancelOrder {
149            program_id: zero,
150            owner: zero,
151            created_by: zero,
152            order_pda: zero,
153            intent_bytes,
154        })
155        .data
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::fixtures::{default_cancel_data, valid_intent_bytes, NUM_ACCOUNTS};
162    use super::*;
163    use crate::fixtures::pubkey_from_seed;
164    use crate::instruction::fixtures::{fake_account, fake_sequential_accounts};
165    use crate::instruction::tests::{
166        assert_readonly_nonsigner, assert_readonly_signer, assert_writable_nonsigner,
167        assert_writable_signer,
168    };
169    use solana_account_view::AccountView;
170
171    #[test]
172    fn cancel_order_input_parses_valid_input() {
173        let program_id = pubkey_from_seed("program id");
174        let owner = pubkey_from_seed("owner");
175        let created_by = pubkey_from_seed("created by");
176        let order_pda = pubkey_from_seed("order pda");
177        let intent_bytes = valid_intent_bytes();
178
179        let data = Instruction::from(CancelOrder {
180            program_id,
181            owner,
182            created_by,
183            order_pda,
184            intent_bytes: Some(intent_bytes),
185        })
186        .data;
187        let accounts = [
188            fake_account(owner),
189            fake_account(created_by),
190            fake_account(order_pda),
191            fake_account(pubkey_from_seed("system program")),
192        ];
193
194        let CancelOrderInput {
195            intent_bytes: derived_intent_bytes,
196            owner: derived_owner,
197            created_by: derived_created_by,
198            order_pda: derived_order_pda,
199        } = CancelOrderInput::parse(&data, &accounts).expect("parse should succeed");
200
201        assert_eq!(derived_intent_bytes, Some(intent_bytes));
202        assert_eq!(*derived_order_pda.address(), order_pda);
203        assert_eq!(*derived_owner.address(), owner);
204        assert_eq!(*derived_created_by.address(), created_by);
205    }
206
207    #[test]
208    fn cancel_order_input_parses_without_intent() {
209        let owner = pubkey_from_seed("owner");
210        let created_by = pubkey_from_seed("created by");
211        let order_pda = pubkey_from_seed("order pda");
212
213        let data = default_cancel_data(None);
214        let accounts = [
215            fake_account(owner),
216            fake_account(created_by),
217            fake_account(order_pda),
218            fake_account(pubkey_from_seed("system program")),
219        ];
220
221        let CancelOrderInput {
222            intent_bytes: derived_intent_bytes,
223            owner: derived_owner,
224            order_pda: derived_order_pda,
225            ..
226        } = CancelOrderInput::parse(&data, &accounts).expect("parse should succeed");
227
228        assert_eq!(
229            derived_intent_bytes, None,
230            "an empty body carries no intent"
231        );
232        assert_eq!(*derived_order_pda.address(), order_pda);
233        assert_eq!(*derived_owner.address(), owner);
234    }
235
236    #[test]
237    fn cancel_order_input_rejects_short_data() {
238        let intent_bytes = valid_intent_bytes();
239        let mut data = default_cancel_data(Some(intent_bytes));
240        data.pop();
241        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
242        assert_eq!(
243            CancelOrderInput::parse(&data, &accounts).err(),
244            Some(ProgramError::InvalidInstructionData),
245        );
246    }
247
248    #[test]
249    fn cancel_order_input_rejects_long_data() {
250        let intent_bytes = valid_intent_bytes();
251        let mut data = default_cancel_data(Some(intent_bytes));
252        data.push(0); // trailing byte
253        let accounts = fake_sequential_accounts::<NUM_ACCOUNTS>();
254        assert_eq!(
255            CancelOrderInput::parse(&data, &accounts).err(),
256            Some(ProgramError::InvalidInstructionData),
257        );
258    }
259
260    #[test]
261    fn cancel_order_input_rejects_missing_accounts() {
262        let intent_bytes = valid_intent_bytes();
263        let data = default_cancel_data(Some(intent_bytes));
264        let mut accounts: Vec<AccountView> = fake_sequential_accounts::<NUM_ACCOUNTS>().into();
265        accounts.pop();
266        assert_eq!(
267            CancelOrderInput::parse(&data, &accounts).err(),
268            Some(ProgramError::NotEnoughAccountKeys),
269        );
270    }
271
272    #[test]
273    fn instruction_data_has_expected_layout() {
274        let program_id = pubkey_from_seed("program id");
275        let owner = pubkey_from_seed("owner");
276        let created_by = pubkey_from_seed("created by");
277        let order_pda = pubkey_from_seed("order pda");
278        let intent_bytes = [0x42u8; EncodedOrderIntent::SIZE];
279
280        let Instruction { data, .. } = CancelOrder {
281            program_id,
282            owner,
283            created_by,
284            order_pda,
285            intent_bytes: Some(intent_bytes),
286        }
287        .into();
288
289        assert_eq!(data.len(), 1 + EncodedOrderIntent::SIZE);
290        assert_eq!(data[0], SettlementInstruction::CancelOrder.discriminator());
291        assert_eq!(&data[1..], &intent_bytes);
292    }
293
294    #[test]
295    fn instruction_data_without_intent_is_discriminator_only() {
296        let Instruction { data, .. } = CancelOrder {
297            program_id: pubkey_from_seed("program id"),
298            owner: pubkey_from_seed("owner"),
299            created_by: pubkey_from_seed("created by"),
300            order_pda: pubkey_from_seed("order pda"),
301            intent_bytes: None,
302        }
303        .into();
304
305        assert_eq!(
306            data,
307            vec![SettlementInstruction::CancelOrder.discriminator()],
308            "an intent-less cancellation carries only the discriminator"
309        );
310    }
311
312    #[test]
313    fn instruction_accounts_without_intent_leave_created_by_unsigned() {
314        let program_id = pubkey_from_seed("program id");
315        let owner = pubkey_from_seed("owner");
316        let created_by = pubkey_from_seed("created by");
317        let order_pda = pubkey_from_seed("order pda");
318
319        let Instruction { accounts, .. } = CancelOrder {
320            program_id,
321            owner,
322            created_by,
323            order_pda,
324            intent_bytes: None,
325        }
326        .into();
327
328        // The account layout is unchanged, but with no rent to fund `created_by`
329        // neither signs nor is written.
330        assert_eq!(accounts.len(), 4);
331        assert_readonly_signer(&accounts[0], owner);
332        assert_readonly_nonsigner(&accounts[1], created_by);
333        assert_writable_nonsigner(&accounts[2], order_pda);
334        assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
335    }
336
337    #[test]
338    fn instruction_data_has_expected_accounts() {
339        let program_id = pubkey_from_seed("program id");
340        let owner = pubkey_from_seed("owner");
341        let created_by = pubkey_from_seed("created by");
342        let order_pda = pubkey_from_seed("order pda");
343        let intent_bytes = [0u8; EncodedOrderIntent::SIZE];
344
345        let Instruction { accounts, .. } = CancelOrder {
346            program_id,
347            owner,
348            created_by,
349            order_pda,
350            intent_bytes: Some(intent_bytes),
351        }
352        .into();
353
354        assert_eq!(accounts.len(), 4);
355        // owner authenticates the cancellation without paying rent; created_by
356        // funds the PDA's rent when the order is created cancelled; order_pda
357        // is the order to cancel. The system program is only referenced.
358        assert_readonly_signer(&accounts[0], owner);
359        assert_writable_signer(&accounts[1], created_by);
360        assert_writable_nonsigner(&accounts[2], order_pda);
361        assert_readonly_nonsigner(&accounts[3], SYSTEM_PROGRAM_ID);
362    }
363}