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 MAKER_DISCRIMINATOR: u8 = 2;
10const MARKET_DISCRIMINATOR: u8 = 4;
11const ACCOUNT_VERSION: u8 = 1;
12
13/// On-chain position status discriminant.
14#[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    /// Decodes a market account owned by `program_id` with the current header and layout.
118    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/// The maker account's identity: its owner and registered quote-signing key.
145#[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    /// Decodes a maker account owned by `program_id` with the current header and layout.
157    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;