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, OrderKind};
32use crate::pda::is_pda_with_signer_seeds;
33use crate::pda::order::order_pda_signer_seeds;
34use crate::{SettlementAccount, 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.address(),
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 pub fn is_fully_filled(&self) -> bool {
93 let (filled, order_amount) =
94 fill_progress(&self.intent, self.amount_withdrawn, self.amount_received);
95 filled >= order_amount
96 }
97}
98
99pub fn fill_progress(
103 intent: &OrderIntent,
104 amount_withdrawn: u64,
105 amount_received: u64,
106) -> (u64, u64) {
107 match intent.flags.kind {
108 OrderKind::Sell => (amount_withdrawn, intent.sell_amount),
109 OrderKind::Buy => (amount_received, intent.buy_amount),
110 }
111}
112
113#[derive(Clone, Debug, Deref, Eq, PartialEq)]
133pub struct EncodedOrderAccount([u8; Self::SIZE]);
134
135impl EncodedOrderAccount {
136 const W_DISCRIMINATOR: usize = size_of::<u8>();
138 const W_BUMP: usize = size_of::<u8>();
139 const W_CANCELLED: usize = size_of::<bool>();
140 const W_AMOUNT_WITHDRAWN: usize = size_of::<u64>();
141 const W_AMOUNT_RECEIVED: usize = size_of::<u64>();
142 const W_CREATED_BY: usize = size_of::<Pubkey>();
143 const W_INTENT: usize = EncodedOrderIntent::SIZE;
144
145 pub const SIZE: usize = 264;
146
147 pub const DISCRIMINATOR: u8 = SettlementAccount::OrderAccount.discriminator();
149
150 pub fn decode_and_hash(bytes: &[u8; Self::SIZE]) -> Result<(OrderAccount, Hash), ProgramError> {
155 let order_account = OrderAccount::try_from(*bytes)?;
156 let (_, raw_intent) = array_refs![
160 bytes,
161 EncodedOrderAccount::SIZE - EncodedOrderAccount::W_INTENT,
162 EncodedOrderAccount::W_INTENT
163 ];
164 let intent_uid = intent::hash_bytes(raw_intent);
165 Ok((order_account, intent_uid))
166 }
167}
168
169pub fn write_account(
173 buffer: &mut [u8; EncodedOrderAccount::SIZE],
174 bump: u8,
175 cancelled: bool,
176 amount_withdrawn: u64,
177 amount_received: u64,
178 created_by: &Pubkey,
179 encoded_intent: &[u8; EncodedOrderIntent::SIZE],
180) {
181 let (
182 discriminator_slot,
183 bump_slot,
184 cancelled_slot,
185 amount_withdrawn_slot,
186 amount_received_slot,
187 created_by_slot,
188 intent_slot,
189 ) = mut_array_refs![
190 buffer,
191 EncodedOrderAccount::W_DISCRIMINATOR,
192 EncodedOrderAccount::W_BUMP,
193 EncodedOrderAccount::W_CANCELLED,
194 EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
195 EncodedOrderAccount::W_AMOUNT_RECEIVED,
196 EncodedOrderAccount::W_CREATED_BY,
197 EncodedOrderAccount::W_INTENT
198 ];
199 *discriminator_slot = [EncodedOrderAccount::DISCRIMINATOR];
200 *bump_slot = [bump];
201 *cancelled_slot = [cancelled as u8];
202 *amount_withdrawn_slot = amount_withdrawn.to_le_bytes();
203 *amount_received_slot = amount_received.to_le_bytes();
204 *created_by_slot = created_by.to_bytes();
205 *intent_slot = *encoded_intent;
206}
207
208impl From<EncodedOrderAccount> for [u8; EncodedOrderAccount::SIZE] {
209 fn from(encoded: EncodedOrderAccount) -> Self {
210 encoded.0
211 }
212}
213
214impl From<OrderAccount> for EncodedOrderAccount {
215 fn from(account: OrderAccount) -> Self {
216 let mut out = [0u8; Self::SIZE];
217 write_account(
218 &mut out,
219 account.bump,
220 account.cancelled,
221 account.amount_withdrawn,
222 account.amount_received,
223 &account.created_by,
224 &EncodedOrderIntent::from(&account.intent),
225 );
226 Self(out)
227 }
228}
229
230impl TryFrom<[u8; EncodedOrderAccount::SIZE]> for OrderAccount {
231 type Error = ProgramError;
232
233 fn try_from(bytes: [u8; EncodedOrderAccount::SIZE]) -> Result<Self, Self::Error> {
234 let (discriminator, bump, cancelled, amount_withdrawn, amount_received, created_by, intent) = array_refs![
235 &bytes,
236 EncodedOrderAccount::W_DISCRIMINATOR,
237 EncodedOrderAccount::W_BUMP,
238 EncodedOrderAccount::W_CANCELLED,
239 EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
240 EncodedOrderAccount::W_AMOUNT_RECEIVED,
241 EncodedOrderAccount::W_CREATED_BY,
242 EncodedOrderAccount::W_INTENT
243 ];
244
245 if *discriminator != [EncodedOrderAccount::DISCRIMINATOR] {
246 return Err(ProgramError::InvalidAccountData);
247 }
248
249 Ok(OrderAccount {
250 bump: bump[0],
251 cancelled: match cancelled {
252 [0] => false,
253 [1] => true,
254 _ => return Err(ProgramError::InvalidAccountData),
255 },
256 amount_withdrawn: u64::from_le_bytes(*amount_withdrawn),
257 amount_received: u64::from_le_bytes(*amount_received),
258 created_by: Pubkey::new_from_array(*created_by),
259 intent: OrderIntent::try_from(intent).map_err(|_| ProgramError::InvalidAccountData)?,
260 })
261 }
262}
263
264impl TryFrom<&[u8]> for OrderAccount {
265 type Error = ProgramError;
266
267 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
268 let bytes: &[u8; EncodedOrderAccount::SIZE] = bytes
269 .try_into()
270 .map_err(|_| ProgramError::InvalidAccountData)?;
271 OrderAccount::try_from(*bytes)
272 }
273}
274
275impl TryFrom<EncodedOrderAccount> for OrderAccount {
276 type Error = ProgramError;
277
278 fn try_from(encoded: EncodedOrderAccount) -> Result<Self, Self::Error> {
279 OrderAccount::try_from(encoded.0)
280 }
281}
282
283#[cfg(any(test, feature = "test-fixtures"))]
284pub mod fixtures {
285 use proptest::prelude::*;
286
287 use super::{OrderAccount, Pubkey};
288 use crate::data::intent::fixtures::{arb_order_intent, sample_intent};
289
290 pub const DISCRIMINATOR_OFFSET: usize = 0;
292 pub const CANCELLED_OFFSET: usize = 2;
293 pub const INTENT_OFFSET: usize = 51;
294
295 pub fn sample_account(cancelled: bool) -> OrderAccount {
297 OrderAccount {
298 bump: 0xfd,
299 cancelled,
300 amount_withdrawn: 0x0112_2334_4556_6778,
301 amount_received: 0x899a_abbc_cdde_eff0,
302 created_by: Pubkey::new_from_array([0x43; 32]),
303 intent: sample_intent(Default::default()),
304 }
305 }
306
307 pub fn arb_order_account() -> impl Strategy<Value = OrderAccount> {
309 (
310 any::<u8>(),
311 any::<bool>(),
312 any::<u64>(),
313 any::<u64>(),
314 any::<[u8; 32]>(),
315 arb_order_intent(),
316 )
317 .prop_map(
318 |(bump, cancelled, amount_withdrawn, amount_received, created_by, intent)| {
319 OrderAccount {
320 bump,
321 cancelled,
322 amount_withdrawn,
323 amount_received,
324 created_by: Pubkey::new_from_array(created_by),
325 intent,
326 }
327 },
328 )
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use core::mem::size_of;
335
336 use super::fixtures::{sample_account, CANCELLED_OFFSET, DISCRIMINATOR_OFFSET, INTENT_OFFSET};
337 use super::*;
338 use crate::data::intent::fixtures::{sample_intent, FLAGS_OFFSET};
339 use crate::data::intent::Flags;
340
341 #[test]
345 fn widths_match_field_sizes() {
346 use core::mem::size_of_val;
347
348 let OrderAccount {
351 bump,
352 cancelled,
353 amount_withdrawn,
354 amount_received,
355 created_by,
356 intent: _intent,
359 } = sample_account(false);
360
361 assert_eq!(EncodedOrderAccount::W_BUMP, size_of_val(&bump));
362 assert_eq!(EncodedOrderAccount::W_CANCELLED, size_of_val(&cancelled));
363 assert_eq!(
364 EncodedOrderAccount::W_AMOUNT_WITHDRAWN,
365 size_of_val(&amount_withdrawn)
366 );
367 assert_eq!(
368 EncodedOrderAccount::W_AMOUNT_RECEIVED,
369 size_of_val(&amount_received)
370 );
371 assert_eq!(EncodedOrderAccount::W_CREATED_BY, size_of_val(&created_by));
372
373 assert_eq!(
374 EncodedOrderAccount::W_INTENT,
375 size_of::<EncodedOrderIntent>()
376 );
377 }
378
379 #[test]
380 fn roundtrip_both_cancelled_states() {
381 for cancelled in [false, true] {
382 let account = sample_account(cancelled);
383 let encoded = EncodedOrderAccount::from(account.clone());
384 let decoded = OrderAccount::try_from(encoded).expect("example must decode");
385 assert_eq!(decoded, account);
386 }
387 }
388
389 #[test]
390 fn is_fully_filled_tracks_the_exact_side_only() {
391 const SELL_AMOUNT: u64 = 1_000;
392 const BUY_AMOUNT: u64 = 2_000;
393
394 let account = |kind, amount_withdrawn, amount_received| OrderAccount {
395 amount_withdrawn,
396 amount_received,
397 intent: OrderIntent {
398 sell_amount: SELL_AMOUNT,
399 buy_amount: BUY_AMOUNT,
400 ..sample_intent(Flags {
401 kind,
402 ..Default::default()
403 })
404 },
405 ..sample_account(false)
406 };
407
408 let cases = [
410 (OrderKind::Sell, SELL_AMOUNT, 0, true), (OrderKind::Buy, 0, BUY_AMOUNT, true), (OrderKind::Sell, u64::MAX, 0, true), (OrderKind::Sell, u64::MAX, u64::MAX, true), (OrderKind::Buy, 0, u64::MAX, true), (OrderKind::Buy, u64::MAX, u64::MAX, true), (OrderKind::Sell, 0, 0, false), (OrderKind::Sell, SELL_AMOUNT - 1, BUY_AMOUNT, false), (OrderKind::Buy, SELL_AMOUNT, BUY_AMOUNT - 1, false), ];
420 for (kind, withdrawn, received, expected) in cases {
421 assert_eq!(
422 account(kind, withdrawn, received).is_fully_filled(),
423 expected,
424 "{kind:?} order withdrawn={withdrawn} received={received}",
425 );
426 }
427 }
428
429 #[test]
430 fn sanity_check_offsets() {
431 fn first_differing_byte(lhs: &[u8], rhs: &[u8]) -> Option<usize> {
432 lhs.iter().zip(rhs).position(|(l, r)| l != r)
433 }
434
435 let mut sample_account_base = sample_account(false);
436 let base: [u8; EncodedOrderAccount::SIZE] =
437 EncodedOrderAccount::from(sample_account_base.clone()).into();
438 let cancelled: [u8; EncodedOrderAccount::SIZE] =
439 EncodedOrderAccount::from(sample_account(true)).into();
440 assert_eq!(
441 first_differing_byte(&base, &cancelled).expect("should differ in the cancelled byte"),
442 CANCELLED_OFFSET
443 );
444
445 let encoded_intent: [u8; EncodedOrderIntent::SIZE] =
447 (&EncodedOrderIntent::from(&sample_account_base.intent)).into();
448 let bitwise_different_encoded_intent: [u8; EncodedOrderIntent::SIZE] =
454 encoded_intent.map(|b| b ^ 0x01);
455 sample_account_base.intent =
456 OrderIntent::try_from(&bitwise_different_encoded_intent).expect("hack should work");
457 let changed_intent: [u8; EncodedOrderAccount::SIZE] =
458 EncodedOrderAccount::from(sample_account_base).into();
459 assert_eq!(
460 first_differing_byte(&base, &changed_intent).expect("should differ in the intent slot"),
461 INTENT_OFFSET
462 );
463 }
464
465 #[test]
466 fn decode_rejects_wrong_discriminator() {
467 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
468 EncodedOrderAccount::from(sample_account(false)).into();
469 bytes[0] ^= 0xff;
470 let err = OrderAccount::try_from(bytes).expect_err("wrong discriminator must be rejected");
471 assert_eq!(err, ProgramError::InvalidAccountData);
472 }
473
474 #[test]
475 fn decode_rejects_non_boolean_cancelled() {
476 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
477 EncodedOrderAccount::from(sample_account(false)).into();
478 for bad in 0x02u8..=0xff {
479 bytes[CANCELLED_OFFSET] = bad;
480 let err = OrderAccount::try_from(bytes)
481 .expect_err("non-boolean cancelled byte must be rejected");
482 assert_eq!(err, ProgramError::InvalidAccountData);
483 }
484 }
485
486 #[test]
487 fn decode_propagates_invalid_intent() {
488 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
489 EncodedOrderAccount::from(sample_account(false)).into();
490 bytes[INTENT_OFFSET + FLAGS_OFFSET] = 0xff;
494 let err = OrderAccount::try_from(bytes)
495 .expect_err("an invalid intent flags byte must propagate as a decode failure");
496 assert_eq!(err, ProgramError::InvalidAccountData);
497 }
498
499 #[test]
500 fn decode_and_hash_catches_errors() {
501 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
502 EncodedOrderAccount::from(sample_account(false)).into();
503 bytes[CANCELLED_OFFSET] = 0xff;
506 let err = EncodedOrderAccount::decode_and_hash(&bytes)
507 .expect_err("decode_and_hash must propagate the try_from error");
508 assert_eq!(err, ProgramError::InvalidAccountData);
509 }
510
511 #[test]
512 fn try_from_exact_length_slice_decodes() {
513 let account = sample_account(false);
514 let bytes: [u8; EncodedOrderAccount::SIZE] =
515 EncodedOrderAccount::from(account.clone()).into();
516
517 let decoded = OrderAccount::try_from(&bytes[..]).expect("exact-length slice must decode");
518 assert_eq!(decoded, account);
519 }
520
521 #[test]
522 fn try_from_wrong_length_slice_is_rejected() {
523 let bytes: [u8; EncodedOrderAccount::SIZE] =
524 EncodedOrderAccount::from(sample_account(false)).into();
525
526 assert_eq!(
527 OrderAccount::try_from(&bytes[..EncodedOrderAccount::SIZE - 1]),
528 Err(ProgramError::InvalidAccountData),
529 );
530
531 let too_long = [bytes.as_slice(), [0].as_slice()].concat();
532 assert_eq!(
533 OrderAccount::try_from(&too_long[..]),
534 Err(ProgramError::InvalidAccountData),
535 );
536 }
537
538 #[test]
539 fn try_from_slice_forwards_decoding_errors() {
540 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
541 EncodedOrderAccount::from(sample_account(false)).into();
542 bytes[CANCELLED_OFFSET] = 0xff;
543 assert_eq!(
544 OrderAccount::try_from(&bytes[..]),
545 Err(ProgramError::InvalidAccountData),
546 );
547 }
548
549 mod load_from_pda {
550 use super::*;
551 use crate::instruction::fixtures::fake_account_with_data;
552 use crate::pda::order::find_order_pda;
553
554 const PROGRAM_ID: Address = Address::new_from_array([0xc0; 32]);
555
556 fn canonical_account(cancelled: bool) -> (OrderAccount, Address) {
559 let mut account = sample_account(cancelled);
560 let (pda_address, bump) = find_order_pda(&PROGRAM_ID, &account.intent.uid());
561 account.bump = bump;
562 (account, pda_address)
563 }
564
565 #[test]
566 fn accepts_the_canonical_pda() {
567 let (account, pda_address) = canonical_account(false);
568 let order_pda = fake_account_with_data(
569 pda_address,
570 &EncodedOrderAccount::from(account.clone())[..],
571 );
572
573 let loaded = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
574 .expect("canonical PDA must load");
575 assert_eq!(loaded, account);
576 }
577
578 #[test]
579 fn rejects_a_non_canonical_address() {
580 let (account, _) = canonical_account(false);
581 let wrong_address = Pubkey::new_from_array([0x42; 32]);
583 let order_pda =
584 fake_account_with_data(wrong_address, &EncodedOrderAccount::from(account)[..]);
585
586 let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
587 .expect_err("a non-canonical address must be rejected");
588 assert_eq!(err, SettlementError::AccountNotDerivable.into());
589 }
590
591 #[test]
592 fn rejects_a_stored_bump_that_does_not_derive_the_address() {
593 let (mut account, pda_address) = canonical_account(false);
594 account.bump = account.bump.wrapping_sub(1);
598 let order_pda =
599 fake_account_with_data(pda_address, &EncodedOrderAccount::from(account)[..]);
600
601 let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
602 .expect_err("a non-canonical bump must be rejected");
603 assert_eq!(err, SettlementError::AccountNotDerivable.into());
604 }
605
606 #[test]
607 fn propagates_decode_errors() {
608 let (account, pda_address) = canonical_account(false);
609 let mut bytes: [u8; EncodedOrderAccount::SIZE] =
610 EncodedOrderAccount::from(account).into();
611 bytes[CANCELLED_OFFSET] = 0xff;
612 let order_pda = fake_account_with_data(pda_address, &bytes);
613
614 let err = OrderAccount::load_from_pda(&order_pda, &PROGRAM_ID)
615 .expect_err("a corrupt account must fail to decode");
616 assert_eq!(err, ProgramError::InvalidAccountData);
617 }
618 }
619
620 #[test]
621 fn direct_write_account_matches_order_account_decoding() {
622 let bump = 254;
623 let cancelled = true;
624 let amount_withdrawn = 1337;
625 let amount_received = 31337;
626 let intent = sample_intent(Default::default());
627 let created_by = Pubkey::new_from_array([0x42u8; 32]);
628
629 let mut buffer = [0u8; EncodedOrderAccount::SIZE];
630 write_account(
631 &mut buffer,
632 bump,
633 cancelled,
634 amount_withdrawn,
635 amount_received,
636 &created_by,
637 &<[u8; EncodedOrderIntent::SIZE]>::from(&EncodedOrderIntent::from(&intent)),
638 );
639 let direct = EncodedOrderAccount(buffer);
640 let via_order_account = EncodedOrderAccount::from(OrderAccount {
641 bump,
642 cancelled,
643 amount_withdrawn,
644 amount_received,
645 created_by,
646 intent,
647 });
648
649 assert_eq!(direct, via_order_account);
650 }
651
652 mod proptest {
654 use ::proptest::{prelude::*, test_runner::TestCaseError};
655
656 use super::*;
657 use crate::data::{intent::fixtures::arb_flags_byte, order::fixtures::arb_order_account};
658
659 proptest! {
660 #[test]
663 fn account_roundtrip(account in arb_order_account()) {
664 let encoded = EncodedOrderAccount::from(account.clone());
665 let decoded = OrderAccount::try_from(encoded)
666 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
667 prop_assert_eq!(decoded, account);
668 }
669
670 #[test]
674 fn bytes_roundtrip(
675 mut bytes in any::<[u8; EncodedOrderAccount::SIZE]>(),
676 cancelled in any::<bool>(),
677 flags in arb_flags_byte(),
678 ) {
679 bytes[DISCRIMINATOR_OFFSET] = EncodedOrderAccount::DISCRIMINATOR;
680 bytes[CANCELLED_OFFSET] = cancelled as u8;
681 bytes[INTENT_OFFSET + FLAGS_OFFSET] = flags;
682 let account = OrderAccount::try_from(bytes)
683 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
684 prop_assert_eq!(*EncodedOrderAccount::from(account), bytes);
685 }
686
687 #[test]
688 fn consistent_decode_and_hash(account in arb_order_account()) {
689 let encoded = EncodedOrderAccount::from(account.clone());
690 let (decoded, hash) = EncodedOrderAccount::decode_and_hash(&encoded)
691 .map_err(|e| TestCaseError::fail(format!("decode failed: {e:?}")))?;
692 prop_assert_eq!(hash, account.intent.uid());
693 prop_assert_eq!(decoded, account);
694 }
695 }
696 }
697}