percli_program/state.rs
1use anchor_lang::prelude::*;
2use percli_core::RiskEngine;
3
4/// Market account header (v1.0 layout) — stored at the beginning of the account data.
5/// The RiskEngine state follows immediately after, accessed via raw pointer
6/// because RiskEngine is ~1.165 MB and doesn't derive Copy.
7///
8/// Layout version: v1 (introduced in percli v1.0.0). v0.9.x markets used a 136-byte
9/// header without `pending_authority` — those accounts must be migrated via
10/// `migrate_header_v1` before any other instruction can run against them.
11#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy)]
12pub struct MarketHeader {
13 /// Authority that created this market (can update params, rotate keys).
14 pub authority: Pubkey,
15 /// SPL token mint for this market's collateral.
16 pub mint: Pubkey,
17 /// Pyth price feed account for this market's oracle.
18 pub oracle: Pubkey,
19 /// Matcher authority — the only signer allowed to submit trades.
20 pub matcher: Pubkey,
21 /// Pending authority for two-step transfer. `Pubkey::default()` when no
22 /// transfer is in flight. Only the holder of this key can call
23 /// `accept_authority` to complete the handoff.
24 pub pending_authority: Pubkey,
25 /// Bump seed for the Market PDA.
26 pub bump: u8,
27 /// Bump seed for the vault token account PDA.
28 pub vault_bump: u8,
29 /// Padding for 8-byte alignment.
30 pub _padding: [u8; 6],
31}
32
33impl MarketHeader {
34 /// Current (v1) header size: 4×Pubkey + pending_authority + 2 bumps + 6 padding = 168 bytes.
35 pub const SIZE: usize = 32 + 32 + 32 + 32 + 32 + 1 + 1 + 6; // 168 bytes
36 /// Legacy (v0) header size — used only by `migrate_header_v1` to detect
37 /// pre-v1.0 accounts that still need to be expanded by 32 bytes.
38 pub const SIZE_V0: usize = 32 + 32 + 32 + 32 + 1 + 1 + 6; // 136 bytes
39}
40
41/// 8-byte discriminator for v1 market accounts: 7 fixed bytes (`b"percmrk"`)
42/// followed by a 1-byte layout version (`0x01`).
43///
44/// Note: percli v0.9 accounts use `b"percmrkt"` (last byte = `0x74` = `'t'`),
45/// the legacy v0 marker. They must be migrated via `migrate_header_v1` before
46/// any other instruction can run against them.
47pub const MARKET_DISCRIMINATOR_V1: [u8; 8] = *b"percmrk\x01";
48
49/// Returns `true` iff `data` starts with the v1 discriminator.
50#[inline]
51pub fn is_v1_market(data: &[u8]) -> bool {
52 data.len() >= 8 && data[0..8] == MARKET_DISCRIMINATOR_V1
53}
54
55/// Total account size (v1 layout): discriminator + header + engine
56pub const MARKET_ACCOUNT_SIZE: usize = 8 + MarketHeader::SIZE + std::mem::size_of::<RiskEngine>();
57
58/// Total account size for the legacy v0.9 layout. Used by `migrate_header_v1`
59/// to validate that the account being migrated is a pre-v1 market.
60pub const MARKET_ACCOUNT_SIZE_V0: usize = 8 + MarketHeader::SIZE_V0 + std::mem::size_of::<RiskEngine>();
61
62/// Market PDA signer seeds: [b"market", authority_key, &[bump]]
63pub fn market_signer_seeds<'a>(
64 authority: &'a Pubkey,
65 bump: &'a [u8; 1],
66) -> [&'a [u8]; 3] {
67 [b"market", authority.as_ref(), bump.as_ref()]
68}
69
70/// Helper to get a mutable reference to the RiskEngine from raw account data.
71///
72/// SAFETY: The caller must ensure the account data is at least
73/// `8 + MarketHeader::SIZE + size_of::<RiskEngine>()` bytes and properly
74/// initialized. The RiskEngine is #[repr(C)] with all-valid bit patterns.
75pub fn engine_from_account_data(data: &mut [u8]) -> &mut RiskEngine {
76 let offset = 8 + MarketHeader::SIZE;
77 let engine_bytes = &mut data[offset..offset + std::mem::size_of::<RiskEngine>()];
78 unsafe { &mut *(engine_bytes.as_mut_ptr() as *mut RiskEngine) }
79}
80
81pub fn engine_from_account_data_ref(data: &[u8]) -> &RiskEngine {
82 let offset = 8 + MarketHeader::SIZE;
83 let engine_bytes = &data[offset..offset + std::mem::size_of::<RiskEngine>()];
84 unsafe { &*(engine_bytes.as_ptr() as *const RiskEngine) }
85}
86
87pub fn header_from_account_data(data: &[u8]) -> std::result::Result<MarketHeader, anchor_lang::error::Error> {
88 let header_bytes = &data[8..8 + MarketHeader::SIZE];
89 MarketHeader::try_from_slice(header_bytes)
90 .map_err(|_| anchor_lang::error!(crate::error::PercolatorError::CorruptState))
91}
92
93pub fn write_header(data: &mut [u8], header: &MarketHeader) {
94 let mut cursor = &mut data[8..8 + MarketHeader::SIZE];
95 header.serialize(&mut cursor).unwrap();
96}