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