1use core::mem::size_of;
22
23use arrayref::{array_refs, mut_array_refs};
24use derive_more::Deref;
25use solana_account_view::AccountView;
26use solana_address::Address;
27use solana_hash::Hash;
28use solana_program_error::ProgramError;
29use solana_pubkey::Pubkey;
30
31use crate::data::intent::{self, EncodedOrderIntent, OrderIntent};
32use crate::pda::is_pda_with_signer_seeds;
33use crate::pda::order::order_pda_signer_seeds;
34use crate::SettlementError;
35
36#[derive(Clone, Debug, Eq, PartialEq, Default)]
38pub struct OrderAccount {
39 pub bump: u8,
42
43 pub cancelled: bool,
46
47 pub amount_withdrawn: u64,
50
51 pub amount_received: u64,
54
55 pub created_by: Pubkey,
58
59 pub intent: OrderIntent,
61}
62
63impl OrderAccount {
64 pub fn load_from_pda(
68 order_pda: &AccountView,
69 program_id: &Address,
70 ) -> Result<Self, ProgramError> {
71 let (account, uid) = {
72 let data = order_pda.try_borrow()?;
73 let bytes: &[u8; EncodedOrderAccount::SIZE] = (&*data)
74 .try_into()
75 .map_err(|_| ProgramError::InvalidAccountData)?;
76 EncodedOrderAccount::decode_and_hash(bytes)?
77 };
78
79 if !is_pda_with_signer_seeds(
80 order_pda,
81 program_id,
82 order_pda_signer_seeds(&uid, &[account.bump]),
83 ) {
84 return Err(SettlementError::AccountNotDerivable.into());
85 }
86
87 Ok(account)
88 }
89}
90
91#[derive(Clone, Debug, Deref, Eq, PartialEq)]
111pub struct EncodedOrderAccount([u8; Self::SIZE]);
112
113impl EncodedOrderAccount {
114 const W_DISCRIMINATOR: usize = size_of::<u8>();
116 const W_BUMP: usize = size_of::<u8>();
117 const W_CANCELLED: usize = size_of::<bool>();
118 const W_AMOUNT_WITHDRAWN: usize = size_of::<u64>();
119 const W_AMOUNT_RECEIVED: usize = size_of::<u64>();
120 const W_CREATED_BY: usize = size_of::<Pubkey>();
121 const W_INTENT: usize = EncodedOrderIntent::SIZE;
122
123 pub const SIZE: usize = 201;
124
125 pub const DISCRIMINATOR: u8 = crate::SettlementAccount::OrderAccount.discriminator();
127
128 pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderAccount, Hash), ProgramError> {
133 let order_account = OrderAccount::try_from(*bytes)?;
134 let (_, raw_intent) = array_refs![
138 bytes,
139 EncodedOrderAccount::SIZE - EncodedOrderAccount::W_INTENT,
140 EncodedOrderAccount::W_INTENT
141 ];
142 let intent_uid = intent::hash_bytes(raw_intent);
143 Ok((order_account, intent_uid))
144 }
145}
146
147pub fn write_account(
151 buffer: &mut [u8; EncodedOrderAccount::SIZE],
152 bump: u8,
153 cancelled: bool,
154 amount_withdrawn: u64,
155 amount_received: u64,
156 created_by: &Pubkey,
157 encoded_intent: &[u8; EncodedOrderIntent::SIZE],
158) {
159 let (
160 discriminator_slot,
161 bump_slot,
162 cancelled_slot,
163 amount_withdrawn_slot,
164 amount_received_slot,
165 created_by_slot,
166 intent_slot,
167 ) = mut_array_refs![
168 buffer,
169 EncodedOrderAccount::W_DISCRIMINATOR,
170 EncodedOrderAccount::W_BUMP,
171 EncodedOrderAccount::W_CANCELLED,
172 EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
173 EncodedOrderAccount::W_AMOUNT_RECEIVED,
174 EncodedOrderAccount::W_CREATED_BY,
175 EncodedOrderAccount::W_INTENT
176 ];
177 *discriminator_slot = [EncodedOrderAccount::DISCRIMINATOR];
178 *bump_slot = [bump];
179 *cancelled_slot = [cancelled as u8];
180 *amount_withdrawn_slot = amount_withdrawn.to_le_bytes();
181 *amount_received_slot = amount_received.to_le_bytes();
182 *created_by_slot = created_by.to_bytes();
183 *intent_slot = *encoded_intent;
184}
185
186impl From<EncodedOrderAccount> for [u8; EncodedOrderAccount::SIZE] {
187 fn from(encoded: EncodedOrderAccount) -> Self {
188 encoded.0
189 }
190}
191
192impl From<OrderAccount> for EncodedOrderAccount {
193 fn from(account: OrderAccount) -> Self {
194 let mut out = [0u8; Self::SIZE];
195 write_account(
196 &mut out,
197 account.bump,
198 account.cancelled,
199 account.amount_withdrawn,
200 account.amount_received,
201 &account.created_by,
202 &EncodedOrderIntent::from(&account.intent),
203 );
204 Self(out)
205 }
206}
207
208impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount {
209 type Error = ProgramError;
210
211 fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result<Self, Self::Error> {
212 let (discriminator, bump, cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
213 &bytes,
214 EncodedOrderAccount::W_DISCRIMINATOR,
215 EncodedOrderAccount::W_BUMP,
216 EncodedOrderAccount::W_CANCELLED,
217 EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
218 EncodedOrderAccount::W_AMOUNT_RECEIVED,
219 EncodedOrderAccount::W_CREATED_BY,
220 EncodedOrderAccount::W_INTENT
221 ];
222
223 if *discriminator != [EncodedOrderAccount::DISCRIMINATOR] {
224 return Err(ProgramError::InvalidAccountData);
225 }
226
227 Ok(OrderAccount {
228 bump: bump[0],
229 cancelled: match cancelled {
230 [0] => false,
231 [1] => true,
232 _ => return Err(ProgramError::InvalidAccountData),
233 },
234 amount_withdrawn: u64::from_le_bytes(*amount_withdrawn),
235 amount_received: u64::from_le_bytes(*amount_received),
236 created_by: Pubkey::new_from_array(*created_by),
237 intent: OrderIntent::try_from(intent).map_err(|_| ProgramError::InvalidAccountData)?,
238 })
239 }
240}
241
242impl TryFrom<&[u8]> for OrderAccount {
243 type Error = ProgramError;
244
245 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
246 let bytes: &[u8; EncodedOrderAccount::SIZE] = bytes
247 .try_into()
248 .map_err(|_| ProgramError::InvalidAccountData)?;
249 OrderAccount::try_from(*bytes)
250 }
251}
252
253impl TryFrom<EncodedOrderAccount> for OrderAccount {
254 type Error = ProgramError;
255
256 fn try_from(encoded: EncodedOrderAccount) -> Result<Self, Self::Error> {
257 OrderAccount::try_from(encoded.0)
258 }
259}
260
261#[cfg(any(test, feature = "test-fixtures"))]
262pub mod fixtures {
263 use proptest::prelude::*;
264
265 use super::{OrderAccount, Pubkey};
266 use crate::data::intent::{
267 fixtures::{arb_order_intent, sample_intent},
268 OrderKind,
269 };
270
271 pub const DISCRIMINATOR_OFFSET: usize = 0;
273 pub const CANCELLED_OFFSET: usize = 2;
274 pub const INTENT_OFFSET: usize = 51;
275
276 pub fn sample_account(cancelled: bool) -> OrderAccount {
278 OrderAccount {
279 bump: 0xfd,
280 cancelled,
281 amount_withdrawn: 0x0112_2334_4556_6778,
282 amount_received: 0x899a_abbc_cdde_eff0,
283 created_by: Pubkey::new_from_array([0x43; 32]),
284 intent: sample_intent(OrderKind::Sell, false),
285 }
286 }
287
288 pub fn arb_order_account() -> impl Strategy<Value = OrderAccount> {
290 (
291 any::<u8>(),
292 any::<bool>(),
293 any::<u64>(),
294 any::<u64>(),
295 any::<[u8; 32]>(),
296 arb_order_intent(),
297 )
298 .prop_map(
299 |(bump, cancelled, amount_withdrawn, amount_received, created_by, intent)| {
300 OrderAccount {
301 bump,
302 cancelled,
303 amount_withdrawn,
304 amount_received,
305 created_by: Pubkey::new_from_array(created_by),
306 intent,
307 }
308 },
309 )
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use core::mem::size_of;
316
317 use super::fixtures::{sample_account, CANCELLED_OFFSET, DISCRIMINATOR_OFFSET, INTENT_OFFSET};
318 use super::*;
319 use crate::data::intent::{
320 fixtures::{sample_intent, KIND_OFFSET, PARTIALLY_FILLABLE_OFFSET},
321 OrderKind,
322 };
323
324 #[test]
328 fn widths_match_field_sizes() {
329 use core::mem::size_of_val;
330
331 let OrderAccount {
334 bump,
335 cancelled,
336 amount_withdrawn,
337 amount_received,
338 created_by,
339 intent: _intent,
342 } = sample_account(false);
343
344 assert_eq!(EncodedOrderAccount::W_BUMP, size_of_val(&bump));
345 assert_eq!(EncodedOrderAccount::W_CANCELLED, size_of_val(&cancelled));
346 assert_eq!(
347 EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
348 size_of_val(&amount_withdrawn)
349 );
350 assert_eq!(
351 EncodedOrderAccount::W_AMOUNT_RECEIVED,
352 size_of_val(&amount_received)
353 );
354 assert_eq!(EncodedOrderAccount::W_CREATED_BY, size_of_val(&created_by));
355
356 assert_eq!(
357 EncodedOrderAccount::W_INTENT,
358 size_of::<EncodedOrderIntent>()
359 );
360 }
361
362 #[test]
363 fn roundtrip_both_cancelled_states() {
364 for cancelled in [false, true] {
365 let account = sample_account(cancelled);
366 let encoded = EncodedOrderAccount::from(account.clone());
367 let decoded = OrderAccount::try_from(encoded).expect("example must decode");
368 assert_eq!(decoded, account);
369 }
370 }
371
372 #[test]
373 fn sanity_check_offsets() {
374 fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
375 lhs.iter().zip(rhs).position(|(l, r)| l != r)
376 }
377
378 let mut sample_account_base = sample_account(false);
379 let base: [u8; EncodedOrderAccount::SIZE] =
380 EncodedOrderAccount::from(sample_account_base.clone()).into();
381 let cancelled: [u8; EncodedOrderAccount::SIZE] =
382 EncodedOrderAccount::from(sample_account(true)).into();
383 assert_eq!(
384 first_differing_byte(&base, &cancelled).expect("should differ in the cancelled byte"),
385 CANCELLED_OFFSET
386 );
387
388 let encoded_intent: [u8; EncodedOrderIntent::SIZE] =
390 (&EncodedOrderIntent::from(&sample_account_base.intent)).into();
391 let bitwise_different_encoded_intent: [u8; EncodedOrderIntent::SIZE] =
397 encoded_intent.map(|b| b ^ 0x01);
398 sample_account_base.intent =
399 OrderIntent::try_from(&bitwise_different_encoded_intent).expect("hack should work");
400 let changed_intent: [u8; EncodedOrderAccount::SIZE] =
401 EncodedOrderAccount::from(sample_account_base).into();
402 assert_eq!(
403 first_differing_byte(&base, &changed_intent).expect("should differ in the intent slot"),
404 INTENT_OFFSET
405 );
406 }
407
408 #[test]
409 fn decode_rejects_wrong_discriminator() {
410 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
411 EncodedOrderAccount::from(sample_account(false)).into();
412 bytes[0] ^= 0xff;
413 let err = OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected");
414 assert_eq!(err, ProgramError::InvalidAccountData);
415 }
416
417 #[test]
418 fn decode_rejects_non_boolean_cancelled() {
419 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
420 EncodedOrderAccount::from(sample_account(false)).into();
421 for bad in 0x02u8..=0xff {
422 bytes[CANCELLED_OFFSET] = bad;
423 let err = OrderAccount::try_from(bytes)
424 .expect_err("non-boolean cancelled byte must be rejected");
425 assert_eq!(err, ProgramError::InvalidAccountData);
426 }
427 }
428
429 #[test]
430 fn decode_propagates_invalid_intent() {
431 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
432 EncodedOrderAccount::from(sample_account(false)).into();
433 let kind_offset = INTENT_OFFSET + KIND_OFFSET;
437 bytes[kind_offset] = 0x02;
438 let err = OrderAccount::try_from(bytes)
439 .expect_err("an invalid intent kind byte must propagate as a decode failure");
440 assert_eq!(err, ProgramError::InvalidAccountData);
441 }
442
443 #[test]
444 fn decode_and_hash_catches_errors() {
445 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
446 EncodedOrderAccount::from(sample_account(false)).into();
447 bytes[CANCELLED_OFFSET] = 0xff;
450 let err = EncodedOrderAccount::decode_and_hash(&bytes)
451 .expect_err("decode_and_hash must propagate the try_from error");
452 assert_eq!(err, ProgramError::InvalidAccountData);
453 }
454
455 #[test]
456 fn try_from_exact_length_slice_decodes() {
457 let account = sample_account(false);
458 let bytes: [u8; EncodedOrderAccount::SIZE] =
459 EncodedOrderAccount::from(account.clone()).into();
460
461 let decoded = OrderAccount::try_from(&bytes[..]).expect("exact-length slice must decode");
462 assert_eq!(decoded, account);
463 }
464
465 #[test]
466 fn try_from_wrong_length_slice_is_rejected() {
467 let bytes: [u8; EncodedOrderAccount::SIZE] =
468 EncodedOrderAccount::from(sample_account(false)).into();
469
470 assert_eq!(
471 OrderAccount::try_from(&bytes[..EncodedOrderAccount::SIZE - 1]),
472 Err(ProgramError::InvalidAccountData),
473 );
474
475 let too_long = [bytes.as_slice(), [0].as_slice()].concat();
476 assert_eq!(
477 OrderAccount::try_from(&too_long[..]),
478 Err(ProgramError::InvalidAccountData),
479 );
480 }
481
482 #[test]
483 fn try_from_slice_forwards_decoding_errors() {
484 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
485 EncodedOrderAccount::from(sample_account(false)).into();
486 bytes[CANCELLED_OFFSET] = 0xff;
487 assert_eq!(
488 OrderAccount::try_from(&bytes[..]),
489 Err(ProgramError::InvalidAccountData),
490 );
491 }
492
493 mod load_from_pda {
494 use super::*;
495 use crate::instruction::fixtures::fake_account_with_data;
496 use crate::pda::order::find_order_pda;
497
498 const PROGRAM_ID: Address = Address::new_from_array([9; 32]);
499
500 fn canonical_account(cancelled: bool) -> (OrderAccount, Address) {
503 let mut account = sample_account(cancelled);
504 let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
505 account.bump = bump;
506 (account, pda_address)
507 }
508
509 #[test]
510 fn accepts_the_canonical_pda() {
511 let (account, pda_address) = canonical_account(false);
512 let order_pda = fake_account_with_data(
513 pda_address,
514 &EncodedOrderAccount::from(account.clone())[..],
515 );
516
517 let loaded = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
518 .expect("canonical PDA must load");
519 assert_eq!(loaded, account);
520 }
521
522 #[test]
523 fn rejects_a_non_canonical_address() {
524 let (account, _) = canonical_account(false);
525 let wrong_address = Pubkey::new_from_array([0x42; 32]);
527 let order_pda =
528 fake_account_with_data(wrong_address, &EncodedOrderAccount::from(account)[..]);
529
530 let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
531 .expect_err("a non-canonical address must be rejected");
532 assert_eq!(err, SettlementError::AccountNotDerivable.into());
533 }
534
535 #[test]
536 fn rejects_a_stored_bump_that_does_not_derive_the_address() {
537 let (mut account, pda_address) = canonical_account(false);
538 account.bump = account.bump.wrapping_sub(1);
542 let order_pda =
543 fake_account_with_data(pda_address, &EncodedOrderAccount::from(account)[..]);
544
545 let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
546 .expect_err("a non-canonical bump must be rejected");
547 assert_eq!(err, SettlementError::AccountNotDerivable.into());
548 }
549
550 #[test]
551 fn propagates_decode_errors() {
552 let (account, pda_address) = canonical_account(false);
553 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
554 EncodedOrderAccount::from(account).into();
555 bytes[CANCELLED_OFFSET] = 0xff;
556 let order_pda = fake_account_with_data(pda_address, &bytes);
557
558 let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
559 .expect_err("a corrupt account must fail to decode");
560 assert_eq!(err, ProgramError::InvalidAccountData);
561 }
562 }
563
564 #[test]
565 fn direct_write_account_matches_order_account_decoding() {
566 let bump = 254;
567 let cancelled = true;
568 let amount_withdrawn = 1337;
569 let amount_received = 31337;
570 let intent = sample_intent(OrderKind::Sell, false);
571 let created_by = Pubkey::new_from_array([0x42u8; 32]);
572
573 let mut buffer = [0u8; EncodedOrderAccount::SIZE];
574 write_account(
575 &mut buffer,
576 bump,
577 cancelled,
578 amount_withdrawn,
579 amount_received,
580 &created_by,
581 &<[u8; EncodedOrderIntent::SIZE]>::from(&EncodedOrderIntent::from(&intent)),
582 );
583 let direct = EncodedOrderAccount(buffer);
584 let via_order_account = EncodedOrderAccount::from(OrderAccount {
585 bump,
586 cancelled,
587 amount_withdrawn,
588 amount_received,
589 created_by,
590 intent,
591 });
592
593 assert_eq!(direct, via_order_account);
594 }
595
596 mod proptest {
598 use ::proptest::{prelude::*, test_runner::TestCaseError};
599
600 use super::*;
601 use crate::data::{intent::fixtures::arb_order_kind, order::fixtures::arb_order_account};
602
603 proptest! {
604 #[test]
607 fn account_roundtrip(account in arb_order_account()) {
608 let encoded = EncodedOrderAccount::from(account.clone());
609 let decoded = OrderAccount::try_from(encoded)
610 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
611 prop_assert_eq!(decoded, account);
612 }
613
614 #[test]
618 fn bytes_roundtrip(
619 mut bytes in any::<[u8; EncodedOrderAccount::SIZE]>(),
620 cancelled in any::<bool>(),
621 kind in arb_order_kind(),
622 partially_fillable in any::<bool>(),
623 ) {
624 bytes[DISCRIMINATOR_OFFSET] = EncodedOrderAccount::DISCRIMINATOR;
625 bytes[CANCELLED_OFFSET] = cancelled as u8;
626 bytes[INTENT_OFFSET + KIND_OFFSET] = kind as u8;
627 bytes[INTENT_OFFSET + PARTIALLY_FILLABLE_OFFSET] = partially_fillable as u8;
628 let account = OrderAccount::try_from(bytes)
629 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
630 prop_assert_eq!(*EncodedOrderAccount::from(account), bytes);
631 }
632
633 #[test]
634 fn consistent_decode_and_hash(account in arb_order_account()) {
635 let encoded = EncodedOrderAccount::from(account.clone());
636 let (decoded, hash) = EncodedOrderAccount::decode_and_hash(&encoded)
637 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
638 prop_assert_eq!(hash, account.intent.uid());
639 prop_assert_eq!(decoded, account);
640 }
641 }
642 }
643}