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