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, 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/// On-chain position status discriminant.
15#[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    /// On a buy the wallet buyer is the `holder` and the maker the `writer`.
46    pub side: TakerSide,
47    pub flags: u8,
48    pub flags2: u8,
49    pub flags3: u8,
50    pub status: PositionStatus,
51    pub writer: Pubkey,
52    pub holder: 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/// The checked global authority used as the keeper for sponsored opens.
73#[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    /// Decodes the fixed current GlobalConfig layout owned by `program_id`.
87    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_WRITER: usize = 8;
112pub(crate) const POSITION_OFFSET_HOLDER: 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: if account.data[5] & POSITION_FLAG_TAKER_BUY != 0 {
152                TakerSide::Buy
153            } else {
154                TakerSide::Sell
155            },
156            flags: account.data[5],
157            flags2: account.data[6],
158            flags3: account.data[7],
159            status: PositionStatus::try_from(account.data[POSITION_OFFSET_STATUS])?,
160            writer: read_pubkey(&account.data, POSITION_OFFSET_WRITER)?,
161            holder: read_pubkey(&account.data, POSITION_OFFSET_HOLDER)?,
162            market_pda: read_pubkey(&account.data, POSITION_OFFSET_MARKET)?,
163            quantity: Quantity::new(read_u64(&account.data, POSITION_OFFSET_QUANTITY)?),
164            order_id: OrderId::new(order_id),
165        })
166    }
167}
168
169const MARKET_OFFSET_UNDERLYING_DECIMALS: usize = 3;
170const MARKET_OFFSET_QUOTE_DECIMALS: usize = 4;
171const MARKET_OFFSET_FLAGS: usize = 5;
172const MARKET_OFFSET_EXPIRY: usize = 8;
173const MARKET_OFFSET_UNDERLYING_MINT: usize = 32;
174const MARKET_OFFSET_QUOTE_MINT: usize = 64;
175const MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM: usize = 96;
176const MARKET_OFFSET_QUOTE_TOKEN_PROGRAM: usize = 128;
177const MARKET_LEN: usize = 416;
178const POSITION_FLAG_TAKER_BUY: u8 = 1 << 2;
179const MARKET_FLAG_PUT: u8 = 1;
180const MARKET_FLAG_FINALIZED: u8 = 2;
181
182impl MarketInfo {
183    /// Decodes a market account owned by `program_id` with the current header and layout.
184    pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
185        let data = current_layout(
186            account,
187            program_id,
188            MARKET_DISCRIMINATOR,
189            ACCOUNT_VERSION,
190            MARKET_LEN,
191        )?;
192        let flags = data[MARKET_OFFSET_FLAGS];
193        if flags & !(MARKET_FLAG_PUT | MARKET_FLAG_FINALIZED) != 0 {
194            return Err(ChainError::InvalidAccountData);
195        }
196        Ok(Self {
197            expiry_ts: read_u64(data, MARKET_OFFSET_EXPIRY)?,
198            is_put: flags & MARKET_FLAG_PUT != 0,
199            is_finalized: flags & MARKET_FLAG_FINALIZED != 0,
200            settlement_price: read_u64(data, 16)?,
201            underlying_decimals: Decimals::new(data[MARKET_OFFSET_UNDERLYING_DECIMALS]),
202            quote_decimals: Decimals::new(data[MARKET_OFFSET_QUOTE_DECIMALS]),
203            underlying_mint: read_pubkey(data, MARKET_OFFSET_UNDERLYING_MINT)?,
204            quote_mint: read_pubkey(data, MARKET_OFFSET_QUOTE_MINT)?,
205            underlying_token_program_id: read_pubkey(data, MARKET_OFFSET_UNDERLYING_TOKEN_PROGRAM)?,
206            quote_token_program_id: read_pubkey(data, MARKET_OFFSET_QUOTE_TOKEN_PROGRAM)?,
207        })
208    }
209}
210
211/// The maker account's identity: its owner and registered quote-signing key.
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub struct MakerInfo {
214    pub owner: Pubkey,
215    pub quote_signer: Pubkey,
216}
217
218const MAKER_OFFSET_OWNER: usize = 4;
219const MAKER_OFFSET_QUOTE_SIGNER: usize = 36;
220const MAKER_LEN: usize = 328;
221
222impl MakerInfo {
223    /// Decodes a maker account owned by `program_id` with the current header and layout.
224    pub fn from_account(account: &Account, program_id: &Pubkey) -> Result<Self, ChainError> {
225        let data = current_layout(
226            account,
227            program_id,
228            MAKER_DISCRIMINATOR,
229            ACCOUNT_VERSION,
230            MAKER_LEN,
231        )?;
232        Ok(Self {
233            owner: read_pubkey(data, MAKER_OFFSET_OWNER)?,
234            quote_signer: read_pubkey(data, MAKER_OFFSET_QUOTE_SIGNER)?,
235        })
236    }
237}
238
239fn current_layout<'a>(
240    account: &'a Account,
241    program_id: &Pubkey,
242    discriminator: u8,
243    version: u8,
244    len: usize,
245) -> Result<&'a [u8], ChainError> {
246    validate_account(account, program_id, discriminator)?;
247    if account.data.len() != len || account.data[ACCOUNT_OFFSET_VERSION] != version {
248        return Err(ChainError::InvalidAccountData);
249    }
250    Ok(&account.data)
251}
252
253fn validate_account(
254    account: &Account,
255    program_id: &Pubkey,
256    expected_discriminator: u8,
257) -> Result<(), ChainError> {
258    if account.owner != *program_id {
259        return Err(ChainError::AccountOwnerMismatch {
260            expected: *program_id,
261            actual: account.owner,
262        });
263    }
264    let discriminator = account
265        .data
266        .get(ACCOUNT_OFFSET_DISCRIMINATOR)
267        .copied()
268        .ok_or(ChainError::InvalidAccountData)?;
269    if discriminator != expected_discriminator {
270        return Err(ChainError::AccountDiscriminatorMismatch {
271            expected: expected_discriminator,
272            actual: discriminator,
273        });
274    }
275    match account.data.get(ACCOUNT_OFFSET_VERSION) {
276        None | Some(0) => return Err(ChainError::UninitializedAccount),
277        Some(_) => {}
278    }
279    Ok(())
280}
281
282fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey, ChainError> {
283    let bytes: [u8; 32] = data
284        .get(offset..offset + 32)
285        .ok_or(ChainError::InvalidAccountData)?
286        .try_into()
287        .map_err(|_| ChainError::InvalidAccountData)?;
288    Ok(Pubkey::new_from_array(bytes))
289}
290
291fn read_u64(data: &[u8], offset: usize) -> Result<u64, ChainError> {
292    let bytes: [u8; 8] = data
293        .get(offset..offset + 8)
294        .ok_or(ChainError::InvalidAccountData)?
295        .try_into()
296        .map_err(|_| ChainError::InvalidAccountData)?;
297    Ok(u64::from_le_bytes(bytes))
298}
299
300#[cfg(test)]
301#[path = "accounts_tests.rs"]
302mod tests;