Skip to main content

acta_maker_sdk/chain/
accounts.rs

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 MARKET_DISCRIMINATOR: u8 = 4;
10
11/// On-chain position status discriminant.
12#[repr(u8)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PositionStatus {
15    None = 0,
16    Open = 1,
17    Funded = 2,
18    Liquidated = 3,
19    Settled = 4,
20}
21
22impl TryFrom<u8> for PositionStatus {
23    type Error = ChainError;
24
25    fn try_from(value: u8) -> Result<Self, Self::Error> {
26        match value {
27            0 => Ok(Self::None),
28            1 => Ok(Self::Open),
29            2 => Ok(Self::Funded),
30            3 => Ok(Self::Liquidated),
31            4 => Ok(Self::Settled),
32            other => Err(ChainError::UnknownPositionStatus(other)),
33        }
34    }
35}
36
37#[derive(Debug, Clone)]
38pub struct PositionInfo {
39    pub position_type: PositionType,
40    pub status: PositionStatus,
41    pub taker_owner: Pubkey,
42    pub maker_owner: Pubkey,
43    pub market_pda: Pubkey,
44    pub strike: Strike,
45    pub quantity: Quantity,
46    pub order_id: OrderId,
47}
48
49#[derive(Debug, Clone)]
50pub struct MarketInfo {
51    pub underlying_mint: Pubkey,
52    pub quote_mint: Pubkey,
53    pub underlying_token_program_id: Pubkey,
54    pub quote_token_program_id: Pubkey,
55    pub underlying_decimals: Decimals,
56    pub quote_decimals: Decimals,
57}
58
59const POSITION_OFFSET_POSITION_TYPE: usize = 3;
60const POSITION_OFFSET_STATUS: usize = 4;
61const POSITION_OFFSET_TAKER_OWNER: usize = 8;
62const POSITION_OFFSET_MAKER_OWNER: usize = 40;
63const POSITION_OFFSET_MARKET: usize = 72;
64const POSITION_OFFSET_STRIKE: usize = 104;
65const POSITION_OFFSET_QUANTITY: usize = 112;
66const POSITION_OFFSET_ORDER_ID: usize = 128;
67const POSITION_MIN_LEN: usize = POSITION_OFFSET_ORDER_ID + 32;
68
69pub(crate) fn parse_position(
70    account: &Account,
71    program_id: &Pubkey,
72) -> Result<PositionInfo, ChainError> {
73    validate_account(account, program_id, POSITION_DISCRIMINATOR)?;
74    if account.data.len() < POSITION_MIN_LEN {
75        return Err(ChainError::InvalidAccountData);
76    }
77    let mut order_id = [0u8; 32];
78    order_id
79        .copy_from_slice(&account.data[POSITION_OFFSET_ORDER_ID..POSITION_OFFSET_ORDER_ID + 32]);
80    Ok(PositionInfo {
81        position_type: PositionType::try_from(account.data[POSITION_OFFSET_POSITION_TYPE])
82            .map_err(|error| ChainError::InvalidPositionType(error.0))?,
83        status: PositionStatus::try_from(account.data[POSITION_OFFSET_STATUS])?,
84        taker_owner: read_pubkey(&account.data, POSITION_OFFSET_TAKER_OWNER)?,
85        maker_owner: read_pubkey(&account.data, POSITION_OFFSET_MAKER_OWNER)?,
86        market_pda: read_pubkey(&account.data, POSITION_OFFSET_MARKET)?,
87        strike: Strike::new(read_u64(&account.data, POSITION_OFFSET_STRIKE)?),
88        quantity: Quantity::new(read_u64(&account.data, POSITION_OFFSET_QUANTITY)?),
89        order_id: OrderId::new(order_id),
90    })
91}
92
93const MARKET_OFFSET_UNDERLYING_DECIMALS: usize = 3;
94const MARKET_OFFSET_QUOTE_DECIMALS: usize = 4;
95const MARKET_OFFSET_UNDERLYING_MINT: usize = 32;
96const MARKET_OFFSET_QUOTE_MINT: usize = 64;
97const MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM: usize = 96;
98const MARKET_OFFSET_QUOTE_TOKEN_PROGRAM: usize = 128;
99const MARKET_MIN_LEN: usize = MARKET_OFFSET_QUOTE_TOKEN_PROGRAM + 32;
100
101pub(crate) fn parse_market(
102    account: &Account,
103    program_id: &Pubkey,
104) -> Result<MarketInfo, ChainError> {
105    validate_account(account, program_id, MARKET_DISCRIMINATOR)?;
106    if account.data.len() < MARKET_MIN_LEN {
107        return Err(ChainError::InvalidAccountData);
108    }
109    Ok(MarketInfo {
110        underlying_decimals: Decimals::new(account.data[MARKET_OFFSET_UNDERLYING_DECIMALS]),
111        quote_decimals: Decimals::new(account.data[MARKET_OFFSET_QUOTE_DECIMALS]),
112        underlying_mint: read_pubkey(&account.data, MARKET_OFFSET_UNDERLYING_MINT)?,
113        quote_mint: read_pubkey(&account.data, MARKET_OFFSET_QUOTE_MINT)?,
114        underlying_token_program_id: read_pubkey(
115            &account.data,
116            MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM,
117        )?,
118        quote_token_program_id: read_pubkey(&account.data, MARKET_OFFSET_QUOTE_TOKEN_PROGRAM)?,
119    })
120}
121
122fn validate_account(
123    account: &Account,
124    program_id: &Pubkey,
125    expected_discriminator: u8,
126) -> Result<(), ChainError> {
127    if account.owner != *program_id {
128        return Err(ChainError::AccountOwnerMismatch {
129            expected: *program_id,
130            actual: account.owner,
131        });
132    }
133    let discriminator = account
134        .data
135        .get(ACCOUNT_OFFSET_DISCRIMINATOR)
136        .copied()
137        .ok_or(ChainError::InvalidAccountData)?;
138    if discriminator != expected_discriminator {
139        return Err(ChainError::AccountDiscriminatorMismatch {
140            expected: expected_discriminator,
141            actual: discriminator,
142        });
143    }
144    // A missing version byte is as uninitialized as a zero one.
145    match account.data.get(ACCOUNT_OFFSET_VERSION) {
146        None | Some(0) => return Err(ChainError::UninitializedAccount),
147        Some(_) => {}
148    }
149    Ok(())
150}
151
152fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey, ChainError> {
153    let bytes: [u8; 32] = data
154        .get(offset..offset + 32)
155        .ok_or(ChainError::InvalidAccountData)?
156        .try_into()
157        .map_err(|_| ChainError::InvalidAccountData)?;
158    Ok(Pubkey::new_from_array(bytes))
159}
160
161fn read_u64(data: &[u8], offset: usize) -> Result<u64, ChainError> {
162    let bytes: [u8; 8] = data
163        .get(offset..offset + 8)
164        .ok_or(ChainError::InvalidAccountData)?
165        .try_into()
166        .map_err(|_| ChainError::InvalidAccountData)?;
167    Ok(u64::from_le_bytes(bytes))
168}
169
170#[cfg(test)]
171#[path = "accounts_tests.rs"]
172mod tests;