1use solana_sdk::{account::Account, pubkey::Pubkey};
2
3use super::ChainError;
4use crate::types::{Decimals, OrderId, PositionType, Quantity, Strike};
5
6const ACCOUNT_OFFSET_DISCRIMINATOR: usize = 0;
7const ACCOUNT_OFFSET_VERSION: usize = 1;
8const POSITION_DISCRIMINATOR: u8 = 1;
9const MAKER_DISCRIMINATOR: u8 = 2;
10const MARKET_DISCRIMINATOR: u8 = 4;
11const ACCOUNT_VERSION: u8 = 1;
12
13#[repr(u8)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PositionStatus {
17 None = 0,
18 Open = 1,
19 Funded = 2,
20 Liquidated = 3,
21 Settled = 4,
22 Closed = 5,
23}
24
25impl TryFrom<u8> for PositionStatus {
26 type Error = ChainError;
27
28 fn try_from(value: u8) -> Result<Self, Self::Error> {
29 match value {
30 0 => Ok(Self::None),
31 1 => Ok(Self::Open),
32 2 => Ok(Self::Funded),
33 3 => Ok(Self::Liquidated),
34 4 => Ok(Self::Settled),
35 5 => Ok(Self::Closed),
36 other => Err(ChainError::UnknownPositionStatus(other)),
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
42pub struct PositionInfo {
43 pub position_type: PositionType,
44 pub status: PositionStatus,
45 pub taker_owner: Pubkey,
46 pub maker_owner: Pubkey,
47 pub market_pda: Pubkey,
48 pub strike: Strike,
49 pub quantity: Quantity,
50 pub order_id: OrderId,
51}
52
53#[derive(Debug, Clone)]
54pub struct MarketInfo {
55 pub expiry_ts: u64,
56 pub is_put: bool,
57 pub is_finalized: bool,
58 pub underlying_mint: Pubkey,
59 pub quote_mint: Pubkey,
60 pub underlying_token_program_id: Pubkey,
61 pub quote_token_program_id: Pubkey,
62 pub underlying_decimals: Decimals,
63 pub quote_decimals: Decimals,
64}
65
66const POSITION_OFFSET_POSITION_TYPE: usize = 3;
67const POSITION_OFFSET_STATUS: usize = 4;
68const POSITION_OFFSET_TAKER_OWNER: usize = 8;
69const POSITION_OFFSET_MAKER_OWNER: usize = 40;
70const POSITION_OFFSET_MARKET: usize = 72;
71const POSITION_OFFSET_STRIKE: usize = 104;
72const POSITION_OFFSET_QUANTITY: usize = 112;
73const POSITION_OFFSET_ORDER_ID: usize = 128;
74const POSITION_LEN: usize = 424;
75const POSITION_VERSION: u8 = 2;
76
77impl PositionInfo {
78 pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
79 current_layout(
80 account,
81 program_id,
82 POSITION_DISCRIMINATOR,
83 POSITION_VERSION,
84 POSITION_LEN,
85 )?;
86 let mut order_id = [0u8; 32];
87 order_id.copy_from_slice(
88 &account.data[POSITION_OFFSET_ORDER_ID..POSITION_OFFSET_ORDER_ID + 32],
89 );
90 Ok(Self {
91 position_type: PositionType::try_from(account.data[POSITION_OFFSET_POSITION_TYPE])
92 .map_err(|error| ChainError::InvalidPositionType(error.0))?,
93 status: PositionStatus::try_from(account.data[POSITION_OFFSET_STATUS])?,
94 taker_owner: read_pubkey(&account.data, POSITION_OFFSET_TAKER_OWNER)?,
95 maker_owner: read_pubkey(&account.data, POSITION_OFFSET_MAKER_OWNER)?,
96 market_pda: read_pubkey(&account.data, POSITION_OFFSET_MARKET)?,
97 strike: Strike::new(read_u64(&account.data, POSITION_OFFSET_STRIKE)?),
98 quantity: Quantity::new(read_u64(&account.data, POSITION_OFFSET_QUANTITY)?),
99 order_id: OrderId::new(order_id),
100 })
101 }
102}
103
104const MARKET_OFFSET_UNDERLYING_DECIMALS: usize = 3;
105const MARKET_OFFSET_QUOTE_DECIMALS: usize = 4;
106const MARKET_OFFSET_FLAGS: usize = 5;
107const MARKET_OFFSET_EXPIRY: usize = 8;
108const MARKET_OFFSET_UNDERLYING_MINT: usize = 32;
109const MARKET_OFFSET_QUOTE_MINT: usize = 64;
110const MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM: usize = 96;
111const MARKET_OFFSET_QUOTE_TOKEN_PROGRAM: usize = 128;
112const MARKET_LEN: usize = 416;
113const MARKET_FLAG_PUT: u8 = 1;
114const MARKET_FLAG_FINALIZED: u8 = 2;
115
116impl MarketInfo {
117 pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
119 let data = current_layout(
120 account,
121 program_id,
122 MARKET_DISCRIMINATOR,
123 ACCOUNT_VERSION,
124 MARKET_LEN,
125 )?;
126 let flags = data[MARKET_OFFSET_FLAGS];
127 if flags & !(MARKET_FLAG_PUT | MARKET_FLAG_FINALIZED) != 0 {
128 return Err(ChainError::InvalidAccountData);
129 }
130 Ok(Self {
131 expiry_ts: read_u64(data, MARKET_OFFSET_EXPIRY)?,
132 is_put: flags & MARKET_FLAG_PUT != 0,
133 is_finalized: flags & MARKET_FLAG_FINALIZED != 0,
134 underlying_decimals: Decimals::new(data[MARKET_OFFSET_UNDERLYING_DECIMALS]),
135 quote_decimals: Decimals::new(data[MARKET_OFFSET_QUOTE_DECIMALS]),
136 underlying_mint: read_pubkey(data, MARKET_OFFSET_UNDERLYING_MINT)?,
137 quote_mint: read_pubkey(data, MARKET_OFFSET_QUOTE_MINT)?,
138 underlying_token_program_id: read_pubkey(data, MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM)?,
139 quote_token_program_id: read_pubkey(data, MARKET_OFFSET_QUOTE_TOKEN_PROGRAM)?,
140 })
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct MakerInfo {
147 pub owner: Pubkey,
148 pub quote_signer: Pubkey,
149}
150
151const MAKER_OFFSET_OWNER: usize = 4;
152const MAKER_OFFSET_QUOTE_SIGNER: usize = 36;
153const MAKER_LEN: usize = 328;
154
155impl MakerInfo {
156 pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
158 let data = current_layout(
159 account,
160 program_id,
161 MAKER_DISCRIMINATOR,
162 ACCOUNT_VERSION,
163 MAKER_LEN,
164 )?;
165 Ok(Self {
166 owner: read_pubkey(data, MAKER_OFFSET_OWNER)?,
167 quote_signer: read_pubkey(data, MAKER_OFFSET_QUOTE_SIGNER)?,
168 })
169 }
170}
171
172fn current_layout<'a>(
173 account: &'a Account,
174 program_id: &Pubkey,
175 discriminator: u8,
176 version: u8,
177 len: usize,
178) -> Result<&'a [u8], ChainError> {
179 validate_account(account, program_id, discriminator)?;
180 if account.data.len() != len || account.data[ACCOUNT_OFFSET_VERSION] != version {
181 return Err(ChainError::InvalidAccountData);
182 }
183 Ok(&account.data)
184}
185
186fn validate_account(
187 account: &Account,
188 program_id: &Pubkey,
189 expected_discriminator: u8,
190) -> Result<(), ChainError> {
191 if account.owner != *program_id {
192 return Err(ChainError::AccountOwnerMismatch {
193 expected: *program_id,
194 actual: account.owner,
195 });
196 }
197 let discriminator = account
198 .data
199 .get(ACCOUNT_OFFSET_DISCRIMINATOR)
200 .copied()
201 .ok_or(ChainError::InvalidAccountData)?;
202 if discriminator != expected_discriminator {
203 return Err(ChainError::AccountDiscriminatorMismatch {
204 expected: expected_discriminator,
205 actual: discriminator,
206 });
207 }
208 match account.data.get(ACCOUNT_OFFSET_VERSION) {
209 None | Some(0) => return Err(ChainError::UninitializedAccount),
210 Some(_) => {}
211 }
212 Ok(())
213}
214
215fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey, ChainError> {
216 let bytes: [u8; 32] = data
217 .get(offset..offset + 32)
218 .ok_or(ChainError::InvalidAccountData)?
219 .try_into()
220 .map_err(|_| ChainError::InvalidAccountData)?;
221 Ok(Pubkey::new_from_array(bytes))
222}
223
224fn read_u64(data: &[u8], offset: usize) -> Result<u64, ChainError> {
225 let bytes: [u8; 8] = data
226 .get(offset..offset + 8)
227 .ok_or(ChainError::InvalidAccountData)?
228 .try_into()
229 .map_err(|_| ChainError::InvalidAccountData)?;
230 Ok(u64::from_le_bytes(bytes))
231}
232
233#[cfg(test)]
234#[path = "accounts_tests.rs"]
235mod tests;