1use solana_sdk::{account::Account, pubkey::Pubkey};
2
3use super::ChainError;
4use crate::types::{Decimals, OptionTerms, OrderId, Quantity, Strike, TakerSide};
5
6const ACCOUNT_OFFSET_DISCRIMINATOR: usize = 0;
7const ACCOUNT_OFFSET_VERSION: usize = 1;
8pub(crate) const POSITION_DISCRIMINATOR: u8 = 1;
9const MAKER_DISCRIMINATOR: u8 = 2;
10const MARKET_DISCRIMINATOR: u8 = 4;
11const GLOBAL_CONFIG_DISCRIMINATOR: u8 = 6;
12const ACCOUNT_VERSION: u8 = 1;
13
14#[repr(u8)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PositionStatus {
18 None = 0,
19 Open = 1,
20 Funded = 2,
21 Liquidated = 3,
22 Settled = 4,
23 Closed = 5,
24}
25
26impl TryFrom<u8> for PositionStatus {
27 type Error = ChainError;
28
29 fn try_from(value: u8) -> Result<Self, Self::Error> {
30 match value {
31 0 => Ok(Self::None),
32 1 => Ok(Self::Open),
33 2 => Ok(Self::Funded),
34 3 => Ok(Self::Liquidated),
35 4 => Ok(Self::Settled),
36 5 => Ok(Self::Closed),
37 other => Err(ChainError::UnknownPositionStatus(other)),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
43pub struct PositionInfo {
44 pub terms: OptionTerms,
45 pub side: TakerSide,
47 pub flags: u8,
48 pub flags2: u8,
49 pub flags3: u8,
50 pub status: PositionStatus,
51 pub taker_owner: Pubkey,
52 pub maker_owner: Pubkey,
53 pub market_pda: Pubkey,
54 pub quantity: Quantity,
55 pub order_id: OrderId,
56}
57
58#[derive(Debug, Clone)]
59pub struct MarketInfo {
60 pub expiry_ts: u64,
61 pub is_put: bool,
62 pub is_finalized: bool,
63 pub settlement_price: u64,
64 pub underlying_mint: Pubkey,
65 pub quote_mint: Pubkey,
66 pub underlying_token_program_id: Pubkey,
67 pub quote_token_program_id: Pubkey,
68 pub underlying_decimals: Decimals,
69 pub quote_decimals: Decimals,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct GlobalConfigInfo {
75 pub hot_authority: Pubkey,
76 pub bump: u8,
77}
78
79const GLOBAL_CONFIG_LEN: usize = 296;
80const GLOBAL_CONFIG_OFFSET_FLAGS: usize = 3;
81const GLOBAL_CONFIG_OFFSET_PAD0: usize = 4;
82const GLOBAL_CONFIG_OFFSET_HOT_AUTHORITY: usize = 8;
83const GLOBAL_CONFIG_KNOWN_FLAGS: u8 = 1;
84
85impl GlobalConfigInfo {
86 pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
88 let data = current_layout(
89 account,
90 program_id,
91 GLOBAL_CONFIG_DISCRIMINATOR,
92 ACCOUNT_VERSION,
93 GLOBAL_CONFIG_LEN,
94 )?;
95 let hot_authority = read_pubkey(data, GLOBAL_CONFIG_OFFSET_HOT_AUTHORITY)?;
96 if data[GLOBAL_CONFIG_OFFSET_FLAGS] & !GLOBAL_CONFIG_KNOWN_FLAGS != 0
97 || data[GLOBAL_CONFIG_OFFSET_PAD0..GLOBAL_CONFIG_OFFSET_HOT_AUTHORITY] != [0; 4]
98 || hot_authority == Pubkey::default()
99 {
100 return Err(ChainError::InvalidAccountData);
101 }
102 Ok(Self {
103 hot_authority,
104 bump: data[2],
105 })
106 }
107}
108
109const POSITION_OFFSET_POSITION_TYPE: usize = 3;
110const POSITION_OFFSET_STATUS: usize = 4;
111const POSITION_OFFSET_TAKER_OWNER: usize = 8;
112pub(crate) const POSITION_OFFSET_MAKER_OWNER: usize = 40;
113const POSITION_OFFSET_MARKET: usize = 72;
114const POSITION_OFFSET_STRIKE: usize = 104;
115const POSITION_OFFSET_QUANTITY: usize = 112;
116const POSITION_OFFSET_ORDER_ID: usize = 128;
117pub(crate) const POSITION_LEN: usize = 424;
118const POSITION_VERSION: u8 = 2;
119
120impl PositionInfo {
121 pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
122 current_layout(
123 account,
124 program_id,
125 POSITION_DISCRIMINATOR,
126 POSITION_VERSION,
127 POSITION_LEN,
128 )?;
129 let mut order_id = [0u8; 32];
130 order_id.copy_from_slice(
131 &account.data[POSITION_OFFSET_ORDER_ID..POSITION_OFFSET_ORDER_ID + 32],
132 );
133 let product =
134 crate::ChainPositionType::try_from(account.data[POSITION_OFFSET_POSITION_TYPE])
135 .map_err(|error| ChainError::InvalidPositionType(error.0))?;
136 let strike = Strike::new(read_u64(&account.data, POSITION_OFFSET_STRIKE)?);
137 let upper = if product.is_spread() {
138 Some(Strike::new(read_u64(&account.data, 168)?))
139 } else {
140 None
141 };
142 let terms = OptionTerms::from_chain(match product {
143 crate::ChainPositionType::CoveredCall => crate::ChainTerms::CoveredCall { strike },
144 crate::ChainPositionType::CashSecuredPut => {
145 crate::ChainTerms::CashSecuredPut { strike }
146 }
147 _ => crate::ChainTerms::from_parts(product, strike, upper)?,
148 });
149 Ok(Self {
150 terms,
151 side: TakerSide::Sell,
152 flags: account.data[5],
153 flags2: account.data[6],
154 flags3: account.data[7],
155 status: PositionStatus::try_from(account.data[POSITION_OFFSET_STATUS])?,
156 taker_owner: read_pubkey(&account.data, POSITION_OFFSET_TAKER_OWNER)?,
157 maker_owner: read_pubkey(&account.data, POSITION_OFFSET_MAKER_OWNER)?,
158 market_pda: read_pubkey(&account.data, POSITION_OFFSET_MARKET)?,
159 quantity: Quantity::new(read_u64(&account.data, POSITION_OFFSET_QUANTITY)?),
160 order_id: OrderId::new(order_id),
161 })
162 }
163}
164
165const MARKET_OFFSET_UNDERLYING_DECIMALS: usize = 3;
166const MARKET_OFFSET_QUOTE_DECIMALS: usize = 4;
167const MARKET_OFFSET_FLAGS: usize = 5;
168const MARKET_OFFSET_EXPIRY: usize = 8;
169const MARKET_OFFSET_UNDERLYING_MINT: usize = 32;
170const MARKET_OFFSET_QUOTE_MINT: usize = 64;
171const MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM: usize = 96;
172const MARKET_OFFSET_QUOTE_TOKEN_PROGRAM: usize = 128;
173const MARKET_LEN: usize = 416;
174const MARKET_FLAG_PUT: u8 = 1;
175const MARKET_FLAG_FINALIZED: u8 = 2;
176
177impl MarketInfo {
178 pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
180 let data = current_layout(
181 account,
182 program_id,
183 MARKET_DISCRIMINATOR,
184 ACCOUNT_VERSION,
185 MARKET_LEN,
186 )?;
187 let flags = data[MARKET_OFFSET_FLAGS];
188 if flags & !(MARKET_FLAG_PUT | MARKET_FLAG_FINALIZED) != 0 {
189 return Err(ChainError::InvalidAccountData);
190 }
191 Ok(Self {
192 expiry_ts: read_u64(data, MARKET_OFFSET_EXPIRY)?,
193 is_put: flags & MARKET_FLAG_PUT != 0,
194 is_finalized: flags & MARKET_FLAG_FINALIZED != 0,
195 settlement_price: read_u64(data, 16)?,
196 underlying_decimals: Decimals::new(data[MARKET_OFFSET_UNDERLYING_DECIMALS]),
197 quote_decimals: Decimals::new(data[MARKET_OFFSET_QUOTE_DECIMALS]),
198 underlying_mint: read_pubkey(data, MARKET_OFFSET_UNDERLYING_MINT)?,
199 quote_mint: read_pubkey(data, MARKET_OFFSET_QUOTE_MINT)?,
200 underlying_token_program_id: read_pubkey(data, MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM)?,
201 quote_token_program_id: read_pubkey(data, MARKET_OFFSET_QUOTE_TOKEN_PROGRAM)?,
202 })
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub struct MakerInfo {
209 pub owner: Pubkey,
210 pub quote_signer: Pubkey,
211}
212
213const MAKER_OFFSET_OWNER: usize = 4;
214const MAKER_OFFSET_QUOTE_SIGNER: usize = 36;
215const MAKER_LEN: usize = 328;
216
217impl MakerInfo {
218 pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
220 let data = current_layout(
221 account,
222 program_id,
223 MAKER_DISCRIMINATOR,
224 ACCOUNT_VERSION,
225 MAKER_LEN,
226 )?;
227 Ok(Self {
228 owner: read_pubkey(data, MAKER_OFFSET_OWNER)?,
229 quote_signer: read_pubkey(data, MAKER_OFFSET_QUOTE_SIGNER)?,
230 })
231 }
232}
233
234fn current_layout<'a>(
235 account: &'a Account,
236 program_id: &Pubkey,
237 discriminator: u8,
238 version: u8,
239 len: usize,
240) -> Result<&'a [u8], ChainError> {
241 validate_account(account, program_id, discriminator)?;
242 if account.data.len() != len || account.data[ACCOUNT_OFFSET_VERSION] != version {
243 return Err(ChainError::InvalidAccountData);
244 }
245 Ok(&account.data)
246}
247
248fn validate_account(
249 account: &Account,
250 program_id: &Pubkey,
251 expected_discriminator: u8,
252) -> Result<(), ChainError> {
253 if account.owner != *program_id {
254 return Err(ChainError::AccountOwnerMismatch {
255 expected: *program_id,
256 actual: account.owner,
257 });
258 }
259 let discriminator = account
260 .data
261 .get(ACCOUNT_OFFSET_DISCRIMINATOR)
262 .copied()
263 .ok_or(ChainError::InvalidAccountData)?;
264 if discriminator != expected_discriminator {
265 return Err(ChainError::AccountDiscriminatorMismatch {
266 expected: expected_discriminator,
267 actual: discriminator,
268 });
269 }
270 match account.data.get(ACCOUNT_OFFSET_VERSION) {
271 None | Some(0) => return Err(ChainError::UninitializedAccount),
272 Some(_) => {}
273 }
274 Ok(())
275}
276
277fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey, ChainError> {
278 let bytes: [u8; 32] = data
279 .get(offset..offset + 32)
280 .ok_or(ChainError::InvalidAccountData)?
281 .try_into()
282 .map_err(|_| ChainError::InvalidAccountData)?;
283 Ok(Pubkey::new_from_array(bytes))
284}
285
286fn read_u64(data: &[u8], offset: usize) -> Result<u64, ChainError> {
287 let bytes: [u8; 8] = data
288 .get(offset..offset + 8)
289 .ok_or(ChainError::InvalidAccountData)?
290 .try_into()
291 .map_err(|_| ChainError::InvalidAccountData)?;
292 Ok(u64::from_le_bytes(bytes))
293}
294
295#[cfg(test)]
296#[path = "accounts_tests.rs"]
297mod tests;