1use core::mem::size_of;
19
20use arrayref::{array_refs, mut_array_refs};
21use derive_more::Deref;
22use solana_hash::Hash;
23use solana_program_error::ProgramError;
24use solana_pubkey::Pubkey;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
28#[repr(u8)]
29pub enum OrderKind {
30 #[default]
31 Sell = 0,
32 Buy = 1,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq, Default)]
36pub struct OrderIntent {
37 pub owner: Pubkey,
41
42 pub buy_token_account: Pubkey,
46
47 pub sell_token_account: Pubkey,
54
55 pub sell_amount: u64,
59
60 pub buy_amount: u64,
64
65 pub valid_to: u32,
68
69 pub kind: OrderKind,
73
74 pub partially_fillable: bool,
79
80 pub app_data: [u8; 32],
84}
85
86#[derive(Clone, Debug, Deref, Eq, PartialEq)]
105pub struct EncodedOrderIntent([u8; Self::SIZE]);
106
107impl EncodedOrderIntent {
108 const WIDTH_OWNER: usize = size_of::<Pubkey>();
110 const WIDTH_BUY_TOKEN: usize = size_of::<Pubkey>();
111 const WIDTH_SELL_TOKEN: usize = size_of::<Pubkey>();
112 const WIDTH_SELL_AMOUNT: usize = size_of::<u64>();
113 const WIDTH_BUY_AMOUNT: usize = size_of::<u64>();
114 const WIDTH_VALID_TO: usize = size_of::<u32>();
115 const WIDTH_KIND: usize = size_of::<OrderKind>();
116 const WIDTH_PARTIALLY_FILLABLE: usize = size_of::<bool>();
117 const WIDTH_APP_DATA: usize = size_of::<[u8; 32]>();
118
119 pub const SIZE: usize = 150;
120
121 pub fn hash(&self) -> Hash {
123 hash_bytes(&self.0)
124 }
125
126 pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderIntent, Hash), ProgramError> {
131 let intent = OrderIntent::try_from(bytes)?;
132 let uid = hash_bytes(bytes);
138 Ok((intent, uid))
139 }
140}
141
142pub fn hash_bytes(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Hash {
143 solana_sha256_hasher::hashv(&[bytes.as_slice()])
144}
145
146impl From<&EncodedOrderIntent> for [u8; EncodedOrderIntent::SIZE] {
147 fn from(encoded: &EncodedOrderIntent) -> Self {
148 encoded.0
149 }
150}
151
152impl From<&OrderIntent> for EncodedOrderIntent {
153 fn from(intent: &OrderIntent) -> Self {
154 let mut out = [0u8; Self::SIZE];
157 let (
158 owner,
159 buy_token,
160 sell_token,
161 sell_amount,
162 buy_amount,
163 valid_to,
164 kind,
165 partially_fillable,
166 app_data,
167 ) = mut_array_refs![
168 &mut out,
169 EncodedOrderIntent::WIDTH_OWNER,
170 EncodedOrderIntent::WIDTH_BUY_TOKEN,
171 EncodedOrderIntent::WIDTH_SELL_TOKEN,
172 EncodedOrderIntent::WIDTH_SELL_AMOUNT,
173 EncodedOrderIntent::WIDTH_BUY_AMOUNT,
174 EncodedOrderIntent::WIDTH_VALID_TO,
175 EncodedOrderIntent::WIDTH_KIND,
176 EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
177 EncodedOrderIntent::WIDTH_APP_DATA
178 ];
179 *owner = intent.owner.to_bytes();
180 *buy_token = intent.buy_token_account.to_bytes();
181 *sell_token = intent.sell_token_account.to_bytes();
182 *sell_amount = intent.sell_amount.to_le_bytes();
183 *buy_amount = intent.buy_amount.to_le_bytes();
184 *valid_to = intent.valid_to.to_le_bytes();
185 *kind = [intent.kind as u8];
186 *partially_fillable = [intent.partially_fillable as u8];
187 *app_data = intent.app_data;
188 Self(out)
189 }
190}
191
192impl TryFrom<&[u8; EncodedOrderIntent::SIZE]> for OrderIntent {
193 type Error = ProgramError;
194
195 fn try_from(bytes: &[u8; EncodedOrderIntent::SIZE]) -> Result<Self, Self::Error> {
196 let (
204 owner,
205 buy_token,
206 sell_token,
207 sell_amount,
208 buy_amount,
209 valid_to,
210 kind,
211 partially_fillable,
212 app_data,
213 ) = array_refs![
214 bytes,
215 EncodedOrderIntent::WIDTH_OWNER,
216 EncodedOrderIntent::WIDTH_BUY_TOKEN,
217 EncodedOrderIntent::WIDTH_SELL_TOKEN,
218 EncodedOrderIntent::WIDTH_SELL_AMOUNT,
219 EncodedOrderIntent::WIDTH_BUY_AMOUNT,
220 EncodedOrderIntent::WIDTH_VALID_TO,
221 EncodedOrderIntent::WIDTH_KIND,
222 EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
223 EncodedOrderIntent::WIDTH_APP_DATA
224 ];
225
226 Ok(OrderIntent {
227 owner: Pubkey::new_from_array(*owner),
228 buy_token_account: Pubkey::new_from_array(*buy_token),
229 sell_token_account: Pubkey::new_from_array(*sell_token),
230 sell_amount: u64::from_le_bytes(*sell_amount),
231 buy_amount: u64::from_le_bytes(*buy_amount),
232 valid_to: u32::from_le_bytes(*valid_to),
233 kind: match kind {
234 [0] => OrderKind::Sell,
235 [1] => OrderKind::Buy,
236 _ => return Err(ProgramError::InvalidInstructionData),
237 },
238 partially_fillable: match partially_fillable {
239 [0] => false,
240 [1] => true,
241 _ => return Err(ProgramError::InvalidInstructionData),
242 },
243 app_data: *app_data,
244 })
245 }
246}
247
248impl TryFrom<&EncodedOrderIntent> for OrderIntent {
249 type Error = ProgramError;
250
251 fn try_from(encoded: &EncodedOrderIntent) -> Result<Self, Self::Error> {
252 OrderIntent::try_from(&encoded.0)
253 }
254}
255
256impl OrderIntent {
257 pub fn uid(&self) -> Hash {
261 EncodedOrderIntent::from(self).hash()
262 }
263}
264
265#[cfg(any(test, feature = "test-fixtures"))]
266pub mod fixtures {
267 use proptest::{prelude::*, strategy::Union};
268
269 use super::{EncodedOrderIntent, OrderIntent, OrderKind, Pubkey};
270
271 pub const ALL_ORDER_KINDS: [OrderKind; 2] = [OrderKind::Sell, OrderKind::Buy];
273
274 pub const KIND_OFFSET: usize = 116;
276 pub const PARTIALLY_FILLABLE_OFFSET: usize = KIND_OFFSET + EncodedOrderIntent::WIDTH_KIND;
277
278 pub fn sample_intent(kind: OrderKind, partially_fillable: bool) -> OrderIntent {
279 OrderIntent {
280 owner: Pubkey::new_from_array([0x11; 32]),
281 buy_token_account: Pubkey::new_from_array([0x22; 32]),
282 sell_token_account: Pubkey::new_from_array([0x33; 32]),
283 sell_amount: 0x0123_4567_89ab_cdef,
284 buy_amount: 0xfedc_ba98_7654_3210,
285 valid_to: 0xdead_beef,
286 kind,
287 partially_fillable,
288 app_data: [0x44; 32],
289 }
290 }
291
292 pub fn arb_order_kind() -> impl Strategy<Value = OrderKind> {
294 Union::new(ALL_ORDER_KINDS.map(Just))
295 }
296
297 pub fn arb_order_intent() -> impl Strategy<Value = OrderIntent> {
299 (
300 any::<[u8; 32]>(),
301 any::<[u8; 32]>(),
302 any::<[u8; 32]>(),
303 any::<u64>(),
304 any::<u64>(),
305 any::<u32>(),
306 arb_order_kind(),
307 any::<bool>(),
308 any::<[u8; 32]>(),
309 )
310 .prop_map(
311 |(owner, buy_tok, sell_tok, sell_amount, buy_amount, valid_to, kind, pf, app)| {
312 OrderIntent {
313 owner: Pubkey::new_from_array(owner),
314 buy_token_account: Pubkey::new_from_array(buy_tok),
315 sell_token_account: Pubkey::new_from_array(sell_tok),
316 sell_amount,
317 buy_amount,
318 valid_to,
319 kind,
320 partially_fillable: pf,
321 app_data: app,
322 }
323 },
324 )
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use hex_literal::hex;
331
332 use super::fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET};
333 use super::*;
334
335 fn all_kind_and_fillable() -> impl Iterator<Item = (OrderKind, bool)> {
338 fixtures::ALL_ORDER_KINDS
339 .into_iter()
340 .flat_map(|kind| core::iter::repeat(kind).zip([false, true]))
341 }
342
343 #[test]
347 fn widths_match_field_sizes() {
348 use core::mem::{size_of, size_of_val};
349
350 let intent = sample_intent(OrderKind::Sell, false);
353
354 assert_eq!(EncodedOrderIntent::WIDTH_OWNER, size_of_val(&intent.owner));
355 assert_eq!(
356 EncodedOrderIntent::WIDTH_BUY_TOKEN,
357 size_of_val(&intent.buy_token_account)
358 );
359 assert_eq!(
360 EncodedOrderIntent::WIDTH_SELL_TOKEN,
361 size_of_val(&intent.sell_token_account)
362 );
363 assert_eq!(
364 EncodedOrderIntent::WIDTH_SELL_AMOUNT,
365 size_of_val(&intent.sell_amount)
366 );
367 assert_eq!(
368 EncodedOrderIntent::WIDTH_BUY_AMOUNT,
369 size_of_val(&intent.buy_amount)
370 );
371 assert_eq!(
372 EncodedOrderIntent::WIDTH_VALID_TO,
373 size_of_val(&intent.valid_to)
374 );
375 assert_eq!(EncodedOrderIntent::WIDTH_KIND, size_of_val(&intent.kind));
376 assert_eq!(
377 EncodedOrderIntent::WIDTH_PARTIALLY_FILLABLE,
378 size_of_val(&intent.partially_fillable)
379 );
380 assert_eq!(
381 EncodedOrderIntent::WIDTH_APP_DATA,
382 size_of_val(&intent.app_data)
383 );
384
385 assert_eq!(EncodedOrderIntent::SIZE, size_of::<EncodedOrderIntent>());
386 }
387
388 #[test]
389 fn roundtrip_all_kind_and_bool_combinations() {
390 for (kind, partially_fillable) in all_kind_and_fillable() {
391 let intent = sample_intent(kind, partially_fillable);
392 let encoded = EncodedOrderIntent::from(&intent);
393 let (decoded, _uid) =
394 EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
395 assert_eq!(decoded, intent);
396 }
397 }
398
399 #[test]
404 fn decode_and_hash_uid_matches_encoded_hash() {
405 for (kind, partially_fillable) in all_kind_and_fillable() {
406 let encoded = EncodedOrderIntent::from(&sample_intent(kind, partially_fillable));
407 let (_intent, uid) =
408 EncodedOrderIntent::decode_and_hash(&encoded).expect("example must decode");
409 assert_eq!(uid, encoded.hash());
410 }
411 }
412
413 #[test]
414 fn sanity_check_offsets() {
415 fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
416 lhs.iter().zip(rhs).position(|(l, r)| l != r)
417 }
418 let sell_false: EncodedOrderIntent = (&sample_intent(OrderKind::Sell, false)).into();
419 let sell_true: EncodedOrderIntent = (&sample_intent(OrderKind::Sell, true)).into();
420 let buy_true: EncodedOrderIntent = (&sample_intent(OrderKind::Buy, true)).into();
421
422 assert_eq!(
423 first_differing_byte(sell_false.as_slice(), sell_true.as_slice())
424 .expect("should have different partially fillable byte"),
425 PARTIALLY_FILLABLE_OFFSET
426 );
427 assert_eq!(
428 first_differing_byte(buy_true.as_slice(), sell_true.as_slice())
429 .expect("should have different kind byte"),
430 KIND_OFFSET
431 );
432 }
433
434 #[test]
435 fn decode_rejects_out_of_range_kind() {
436 let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Sell, false));
437 let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
438 for bad in 0x02u8..=0xff {
439 bytes[KIND_OFFSET] = bad;
440 let err = EncodedOrderIntent::decode_and_hash(&bytes)
441 .expect_err("should reject out of range kind");
442 assert_eq!(err, ProgramError::InvalidInstructionData);
443 }
444 }
445
446 #[test]
447 fn decode_rejects_non_boolean_partially_fillable() {
448 let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Sell, false));
449 let mut bytes: [u8; EncodedOrderIntent::SIZE] = *encoded;
450 for bad in 0x02u8..=0xff {
451 bytes[PARTIALLY_FILLABLE_OFFSET] = bad;
452 let err = EncodedOrderIntent::decode_and_hash(&bytes)
453 .expect_err("should reject out of range partially fillable");
454 assert_eq!(err, ProgramError::InvalidInstructionData);
455 }
456 }
457
458 #[test]
459 fn uid_digest_regression() {
460 let intent = sample_intent(OrderKind::Buy, true);
461 let expected = hex!("7ce7c6a74671090771fa33851387444064aca759ce55b80708723076722f5e00");
462 assert_eq!(intent.uid(), Hash::from(expected));
463 }
464
465 #[test]
466 fn encoding_regression() {
467 let encoded = EncodedOrderIntent::from(&sample_intent(OrderKind::Buy, true));
468 let encoding: [u8; EncodedOrderIntent::SIZE] = *encoded;
469 #[rustfmt::skip]
470 let expected: [u8; EncodedOrderIntent::SIZE] = [
471 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
473 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
474 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
475 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
476 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
478 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
479 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
480 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
481 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
483 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
484 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
485 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
486 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01,
488 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe,
490 0xef, 0xbe, 0xad, 0xde,
492 0x01,
494 0x01,
496 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
498 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
499 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
500 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
501 ];
502 assert_eq!(encoding, expected);
503 }
504
505 mod proptest {
507 use ::proptest::{prelude::*, test_runner::TestCaseError};
508
509 use super::*;
510 use crate::data::intent::fixtures::{
511 arb_order_intent, arb_order_kind, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET,
512 };
513
514 fn arb_bad_order_kind_byte() -> impl Strategy<Value = u8> {
516 2u8..=255
517 }
518
519 fn arb_bad_bool_byte() -> impl Strategy<Value = u8> {
521 2u8..=255
522 }
523
524 proptest! {
525 #[test]
530 fn intent_roundtrip(intent in arb_order_intent()) {
531 let encoded = EncodedOrderIntent::from(&intent);
532 let (decoded, uid) = EncodedOrderIntent::decode_and_hash(&encoded)
533 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
534 prop_assert_eq!(decoded, intent);
535 prop_assert_eq!(uid, encoded.hash());
536 }
537
538 #[test]
542 fn bytes_roundtrip(
543 mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
544 kind in arb_order_kind(),
545 partially_fillable in any::<bool>(),
546 ) {
547 bytes[KIND_OFFSET] = kind as u8;
548 bytes[PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
549 let (intent, _uid) = EncodedOrderIntent::decode_and_hash(&bytes)
550 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
551 prop_assert_eq!(*EncodedOrderIntent::from(&intent), bytes);
552 }
553
554 #[test]
558 fn rejects_invalid_kind_byte(
559 mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
560 bad_kind in arb_bad_order_kind_byte(),
561 partially_fillable in any::<bool>(),
562 ) {
563 bytes[KIND_OFFSET] = bad_kind;
564 bytes[PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
565 prop_assert_eq!(
566 EncodedOrderIntent::decode_and_hash(&bytes),
567 Err(ProgramError::InvalidInstructionData),
568 );
569 }
570
571 #[test]
575 fn rejects_invalid_partially_fillable_byte(
576 mut bytes in any::<[u8; EncodedOrderIntent::SIZE]>(),
577 kind in arb_order_kind(),
578 bad_pf in arb_bad_bool_byte(),
579 ) {
580 bytes[KIND_OFFSET] = kind as u8;
581 bytes[PARTIALLY_FILLABLE_OFFSET] = bad_pf;
582 prop_assert_eq!(
583 EncodedOrderIntent::decode_and_hash(&bytes),
584 Err(ProgramError::InvalidInstructionData),
585 );
586 }
587 }
588 }
589}