Skip to main content

cow_settlement_interface/instruction/
mod.rs

1//! Off-chain instruction builders for the settlement program.
2//!
3//! Each submodule builds the [`solana_instruction::Instruction`] for specific
4//! settlement instructions, encoding their discriminator (see
5//! [`crate::SettlementInstruction`]) and laying out the required accounts.
6
7use solana_program_error::ProgramError;
8
9use crate::{recover_discriminator, SettlementInstruction};
10
11pub mod create_buffer;
12pub mod create_order;
13pub mod initialize;
14pub mod reclaim_buffer;
15pub mod reclaim_order;
16pub mod settle;
17
18/// Shared components for parsing an instruction's input (data fields and
19/// accounts).
20///
21/// Implementations declare which [`SettlementInstruction`] discriminator they
22/// belong to and parse the remaining instruction data and accounts. The
23/// discriminator check is shared via the default [`Self::parse`] implementation;
24/// an impl only needs to provide [`Self::parse_body`].
25///
26/// Parsing accounts is purely positional: they are picked out by their index in
27/// the slice, never by inspecting them. The account type `A` is left generic so
28/// the same layout can be parsed from any account representation, on-chain
29/// (`solana_account_view::AccountView`) or off-chain.
30pub trait InstructionInputParsing<'a, A>: Sized {
31    const DISCRIMINATOR: SettlementInstruction;
32
33    fn parse_body(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError>;
34
35    fn parse(instruction_data: &'a [u8], accounts: &'a [A]) -> Result<Self, ProgramError> {
36        match recover_discriminator(instruction_data)? {
37            (discriminator, remaining_data) if discriminator == Self::DISCRIMINATOR => {
38                Self::parse_body(remaining_data, accounts)
39            }
40            _ => Err(ProgramError::InvalidInstructionData),
41        }
42    }
43}
44
45/// Account-building scaffolding shared by the parser unit tests in this crate
46/// and the settlement program's own tests.
47///
48/// Exposed under the `test-fixtures` feature (and unconditionally for this
49/// crate's own `cargo test`) so both crates can build
50/// [`AccountView`](solana_account_view::AccountView)s without duplicating the
51/// unsafe initializer below.
52#[cfg(any(test, feature = "test-fixtures"))]
53pub mod fixtures {
54    use solana_account_view::{AccountView, RuntimeAccount};
55    use solana_address::Address;
56    use std::{mem, ptr};
57
58    /// Build an `AccountView` based on the input `RuntimeAccount` and whose
59    /// data region is empty.
60    ///
61    /// This is trickier to do than it should be. There's no safe initializer for
62    /// `AccountView` in Pinocchio. The only initializer is:
63    /// <https://docs.rs/solana-account-view/2.0.0/solana_account_view/struct.AccountView.html#method.new_unchecked>
64    ///
65    /// `AccountView::new_unchecked` requires (1) a pointer to an initialized
66    /// `RuntimeAccount`, (2) immediately followed by exactly `data_len` bytes of
67    /// data. We satisfy (1) via `Box::new(RuntimeAccount::default())` (every
68    /// field is zero-initialized, then we overwrite `address`), and (2) by
69    /// setting `data_len = 0` so the trailing-data clause is vacuously true
70    /// regardless of what's actually in memory after the box.
71    ///
72    /// [`Box::leak`] keeps the backing alive for the rest of the test process:
73    /// a dropped `Box` or a returned stack slot would leave the pointer
74    /// dangling. We ignore the memory leak since this function is only intended to
75    /// use in tests.
76    /// <https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak>
77    ///
78    /// Every `AccountView` method is safe to call on the result. Header
79    /// accessors read fields out of the `RuntimeAccount`. Data-region accessors
80    /// hand out a zero-length slice, which [`core::slice::from_raw_parts`] (the
81    /// primitive underneath them) defines as sound for any non-null, aligned
82    /// pointer. This is true for us because the pointer itself comes boxed data
83    /// and not some manual allocation.
84    /// <https://docs.rs/crate/solana-account-view/2.0.0/source/src/lib.rs#98-295>
85    /// <https://doc.rust-lang.org/beta/core/slice/fn.from_raw_parts.html>
86    pub fn fake_account_from(runtime_account: RuntimeAccount) -> AccountView {
87        let backing = Box::leak(Box::new(runtime_account));
88        unsafe { AccountView::new_unchecked(backing as *mut RuntimeAccount) }
89    }
90
91    /// Adapted from pinocchio's crate-private test helper; kept structurally
92    /// close for comparison:
93    /// <https://docs.rs/crate/pinocchio/0.11.1/source/src/sysvars/slot_hashes/test_utils.rs#120-160>
94    ///
95    /// Allocate a heap-backed `AccountView` whose data region is initialized with
96    /// `data`, whose address is `address`, whose owning program is `owner`, and
97    /// whose borrow flag is `borrow_state`.
98    ///
99    /// The function also returns the backing `Vec<u64>` so the caller can keep it
100    /// alive for the duration of the test (otherwise the memory would be freed and
101    /// the raw pointer inside `AccountView` would dangle).
102    ///
103    /// # Safety
104    /// The caller must ensure the returned `AccountView` is used only for reading
105    /// or according to borrow rules because the Solana runtime invariants are not
106    /// fully enforced in this hand-rolled representation.
107    #[allow(
108        clippy::arithmetic_side_effects,
109        reason = "the function is mostly vendored and we don't want to introduce unnecessary changes"
110    )]
111    pub unsafe fn make_account_view(
112        address: Address,
113        owner: Address,
114        data: &[u8],
115        borrow_state: u8,
116    ) -> (AccountView, Vec<u64>) {
117        // pinocchio writes a hand-rolled `AccountLayout` mirror and casts the
118        // pointer to `*mut RuntimeAccount`, because inside that crate the
119        // account struct's fields are private and its tests cannot set them by
120        // name. Here we instead write a real `RuntimeAccount`, since
121        // `solana_account_view::RuntimeAccount` has public fields and a
122        // `Default` impl.
123        let hdr_size = mem::size_of::<RuntimeAccount>();
124        let total = hdr_size + data.len();
125        let words = total.div_ceil(8);
126        let mut backing: Vec<u64> = vec![0u64; words];
127        assert!(
128            mem::align_of::<u64>() >= mem::align_of::<RuntimeAccount>(),
129            "`backing` should be properly aligned to store a `RuntimeAccount` instance"
130        );
131
132        let hdr_ptr = backing.as_mut_ptr() as *mut RuntimeAccount;
133        ptr::write(
134            hdr_ptr,
135            RuntimeAccount {
136                address,
137                owner,
138                borrow_state,
139                data_len: data.len() as u64,
140                ..Default::default()
141            },
142        );
143
144        ptr::copy_nonoverlapping(
145            data.as_ptr(),
146            (hdr_ptr as *mut u8).add(hdr_size),
147            data.len(),
148        );
149
150        (AccountView::new_unchecked(hdr_ptr), backing)
151    }
152
153    /// Create an account view storing the input data, owned by `owner`.
154    pub fn fake_account_owned_by(address: Address, owner: Address, data: &[u8]) -> AccountView {
155        let (account, backing) =
156            unsafe { make_account_view(address, owner, data, solana_account_view::NOT_BORROWED) };
157        core::mem::forget(backing);
158        account
159    }
160
161    /// Create an account view storing the input data.
162    pub fn fake_account_with_data(address: Address, data: &[u8]) -> AccountView {
163        fake_account_owned_by(address, Address::default(), data)
164    }
165
166    pub fn fake_account(address: Address) -> AccountView {
167        fake_account_with_data(address, &[])
168    }
169
170    pub fn fake_account_from_array(address_array: [u8; 32]) -> AccountView {
171        fake_account(Address::new_from_array(address_array))
172    }
173
174    pub fn fake_signer(address: Address) -> AccountView {
175        fake_account_from(RuntimeAccount {
176            address,
177            is_signer: 1,
178            ..Default::default()
179        })
180    }
181
182    /// Build `N` fake accounts with sequential addresses (`[1; 32]`, `[2; 32]`, …).
183    pub fn fake_sequential_accounts<const N: usize>() -> [AccountView; N] {
184        core::array::from_fn(|i| fake_account_from_array([(i as u8).wrapping_add(1); 32]))
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::{AccountMeta, Pubkey};
192    use solana_account_view::AccountView;
193
194    #[track_caller]
195    pub(crate) fn assert_writable_signer(account: &AccountMeta, pubkey: Pubkey) {
196        assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
197        assert!(account.is_writable, "{} should be writable", account.pubkey);
198        assert!(account.is_signer, "{} should be a signer", account.pubkey);
199    }
200
201    #[track_caller]
202    pub(crate) fn assert_writable_nonsigner(account: &AccountMeta, pubkey: Pubkey) {
203        assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
204        assert!(account.is_writable, "{} should be writable", account.pubkey);
205        assert!(
206            !account.is_signer,
207            "{} should not be a signer",
208            account.pubkey
209        );
210    }
211
212    #[track_caller]
213    pub(crate) fn assert_readonly_signer(account: &AccountMeta, pubkey: Pubkey) {
214        assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
215        assert!(
216            !account.is_writable,
217            "{} should be read-only",
218            account.pubkey
219        );
220        assert!(account.is_signer, "{} should be a signer", account.pubkey);
221    }
222
223    #[track_caller]
224    pub(crate) fn assert_readonly_nonsigner(account: &AccountMeta, pubkey: Pubkey) {
225        assert_eq!(account.pubkey, pubkey, "unexpected account pubkey");
226        assert!(
227            !account.is_writable,
228            "{} should be read-only",
229            account.pubkey
230        );
231        assert!(
232            !account.is_signer,
233            "{} should not be a signer",
234            account.pubkey
235        );
236    }
237
238    #[test]
239    fn input_parsing_rejects_different_discriminator() {
240        struct TestInputParsing {}
241        impl<'a> InstructionInputParsing<'a, AccountView> for TestInputParsing {
242            const DISCRIMINATOR: SettlementInstruction = SettlementInstruction::BeginSettle;
243
244            fn parse_body(
245                _instruction_data: &'a [u8],
246                _accounts: &'a [AccountView],
247            ) -> Result<Self, ProgramError> {
248                Ok(Self {})
249            }
250        }
251
252        let mut data = [0; 42];
253        let different_discriminator = SettlementInstruction::CreateOrder;
254        assert_ne!(TestInputParsing::DISCRIMINATOR, different_discriminator);
255        data[0] = different_discriminator.discriminator();
256        assert_eq!(
257            TestInputParsing::parse(&data, &[]).err(),
258            Some(ProgramError::InvalidInstructionData),
259        );
260    }
261}