Skip to main content

lightcone_sdk/program/
accounts.rs

1//! Account structures and deserialization for Lightcone Pinocchio.
2//!
3//! This module contains all on-chain account structures with their exact
4//! byte layouts matching the program.
5
6use solana_sdk::pubkey::Pubkey;
7
8use crate::program::constants::{
9    EXCHANGE_DISCRIMINATOR, EXCHANGE_SIZE, MARKET_DISCRIMINATOR, MARKET_SIZE,
10    ORDER_STATUS_DISCRIMINATOR, ORDER_STATUS_SIZE, POSITION_DISCRIMINATOR, POSITION_SIZE,
11    USER_NONCE_DISCRIMINATOR, USER_NONCE_SIZE,
12};
13use crate::program::error::{SdkError, SdkResult};
14use crate::program::types::MarketStatus;
15
16/// Helper to extract a fixed-size array from a slice
17#[inline]
18fn read_bytes<const N: usize>(data: &[u8], offset: usize) -> [u8; N] {
19    let mut arr = [0u8; N];
20    arr.copy_from_slice(&data[offset..offset + N]);
21    arr
22}
23
24/// Helper to read a Pubkey from data
25#[inline]
26fn read_pubkey(data: &[u8], offset: usize) -> Pubkey {
27    Pubkey::new_from_array(read_bytes::<32>(data, offset))
28}
29
30/// Helper to read a u64 from data (little-endian)
31#[inline]
32fn read_u64(data: &[u8], offset: usize) -> u64 {
33    u64::from_le_bytes(read_bytes::<8>(data, offset))
34}
35
36// ============================================================================
37// Exchange Account (88 bytes)
38// ============================================================================
39
40/// Exchange account - singleton state for the exchange
41///
42/// Layout:
43/// - [0..8]   discriminator (8 bytes)
44/// - [8..40]  authority (32 bytes)
45/// - [40..72] operator (32 bytes)
46/// - [72..80] market_count (8 bytes)
47/// - [80]     paused (1 byte)
48/// - [81]     bump (1 byte)
49/// - [82..88] _padding (6 bytes)
50#[derive(Debug, Clone)]
51pub struct Exchange {
52    /// Account discriminator
53    pub discriminator: [u8; 8],
54    /// Exchange authority (can pause, set operator, create markets)
55    pub authority: Pubkey,
56    /// Operator (can match orders)
57    pub operator: Pubkey,
58    /// Number of markets created
59    pub market_count: u64,
60    /// Whether the exchange is paused
61    pub paused: bool,
62    /// PDA bump seed
63    pub bump: u8,
64}
65
66impl Exchange {
67    /// Account size in bytes
68    pub const LEN: usize = EXCHANGE_SIZE;
69
70    /// Deserialize from account data
71    pub fn deserialize(data: &[u8]) -> SdkResult<Self> {
72        if data.len() < Self::LEN {
73            return Err(SdkError::InvalidDataLength {
74                expected: Self::LEN,
75                actual: data.len(),
76            });
77        }
78
79        let discriminator = read_bytes::<8>(data, 0);
80        if discriminator != EXCHANGE_DISCRIMINATOR {
81            return Err(SdkError::InvalidDiscriminator {
82                expected: String::from_utf8_lossy(&EXCHANGE_DISCRIMINATOR).to_string(),
83                actual: String::from_utf8_lossy(&discriminator).to_string(),
84            });
85        }
86
87        Ok(Self {
88            discriminator,
89            authority: read_pubkey(data, 8),
90            operator: read_pubkey(data, 40),
91            market_count: read_u64(data, 72),
92            paused: data[80] != 0,
93            bump: data[81],
94        })
95    }
96
97    /// Check if account data has the exchange discriminator
98    pub fn is_exchange_account(data: &[u8]) -> bool {
99        data.len() >= 8 && data[0..8] == EXCHANGE_DISCRIMINATOR
100    }
101}
102
103// ============================================================================
104// Market Account (120 bytes)
105// ============================================================================
106
107/// Market account - represents a market
108///
109/// Layout:
110/// - [0..8]     discriminator (8 bytes)
111/// - [8..16]   market_id (8 bytes)
112/// - [16]       num_outcomes (1 byte)
113/// - [17]       status (1 byte)
114/// - [18]       winning_outcome (1 byte)
115/// - [19]       has_winning_outcome (1 byte)
116/// - [20]       bump (1 byte)
117/// - [21..24]   _padding (3 bytes)
118/// - [24..56]   oracle (32 bytes)
119/// - [56..88]   question_id (32 bytes)
120/// - [88..120]  condition_id (32 bytes)
121#[derive(Debug, Clone)]
122pub struct Market {
123    /// Account discriminator
124    pub discriminator: [u8; 8],
125    /// Unique market ID
126    pub market_id: u64,
127    /// Number of possible outcomes (2-6)
128    pub num_outcomes: u8,
129    /// Current market status
130    pub status: MarketStatus,
131    /// Winning outcome index (255 if not resolved)
132    pub winning_outcome: u8,
133    /// Whether a winning outcome has been set
134    pub has_winning_outcome: bool,
135    /// PDA bump seed
136    pub bump: u8,
137    /// Oracle pubkey that can settle this market
138    pub oracle: Pubkey,
139    /// Question ID (32 bytes)
140    pub question_id: [u8; 32],
141    /// Condition ID derived from oracle + question_id + num_outcomes
142    pub condition_id: [u8; 32],
143}
144
145impl Market {
146    /// Account size in bytes
147    pub const LEN: usize = MARKET_SIZE;
148
149    /// Deserialize from account data
150    pub fn deserialize(data: &[u8]) -> SdkResult<Self> {
151        if data.len() < Self::LEN {
152            return Err(SdkError::InvalidDataLength {
153                expected: Self::LEN,
154                actual: data.len(),
155            });
156        }
157
158        let discriminator = read_bytes::<8>(data, 0);
159        if discriminator != MARKET_DISCRIMINATOR {
160            return Err(SdkError::InvalidDiscriminator {
161                expected: String::from_utf8_lossy(&MARKET_DISCRIMINATOR).to_string(),
162                actual: String::from_utf8_lossy(&discriminator).to_string(),
163            });
164        }
165
166        Ok(Self {
167            discriminator,
168            market_id: read_u64(data, 8),
169            num_outcomes: data[16],
170            status: MarketStatus::try_from(data[17])?,
171            winning_outcome: data[18],
172            has_winning_outcome: data[19] != 0,
173            bump: data[20],
174            oracle: read_pubkey(data, 24),
175            question_id: read_bytes::<32>(data, 56),
176            condition_id: read_bytes::<32>(data, 88),
177        })
178    }
179
180    /// Check if account data has the market discriminator
181    pub fn is_market_account(data: &[u8]) -> bool {
182        data.len() >= 8 && data[0..8] == MARKET_DISCRIMINATOR
183    }
184}
185
186// ============================================================================
187// Position Account (80 bytes)
188// ============================================================================
189
190/// Position account - user's custody account for a market
191///
192/// Layout:
193/// - [0..8]   discriminator (8 bytes)
194/// - [8..40]  owner (32 bytes)
195/// - [40..72] market (32 bytes)
196/// - [72]     bump (1 byte)
197/// - [73..80] _padding (7 bytes)
198#[derive(Debug, Clone)]
199pub struct Position {
200    /// Account discriminator
201    pub discriminator: [u8; 8],
202    /// Owner of this position
203    pub owner: Pubkey,
204    /// Market this position is for
205    pub market: Pubkey,
206    /// PDA bump seed
207    pub bump: u8,
208}
209
210impl Position {
211    /// Account size in bytes
212    pub const LEN: usize = POSITION_SIZE;
213
214    /// Deserialize from account data
215    pub fn deserialize(data: &[u8]) -> SdkResult<Self> {
216        if data.len() < Self::LEN {
217            return Err(SdkError::InvalidDataLength {
218                expected: Self::LEN,
219                actual: data.len(),
220            });
221        }
222
223        let discriminator = read_bytes::<8>(data, 0);
224        if discriminator != POSITION_DISCRIMINATOR {
225            return Err(SdkError::InvalidDiscriminator {
226                expected: String::from_utf8_lossy(&POSITION_DISCRIMINATOR).to_string(),
227                actual: String::from_utf8_lossy(&discriminator).to_string(),
228            });
229        }
230
231        Ok(Self {
232            discriminator,
233            owner: read_pubkey(data, 8),
234            market: read_pubkey(data, 40),
235            bump: data[72],
236        })
237    }
238
239    /// Check if account data has the position discriminator
240    pub fn is_position_account(data: &[u8]) -> bool {
241        data.len() >= 8 && data[0..8] == POSITION_DISCRIMINATOR
242    }
243}
244
245// ============================================================================
246// OrderStatus Account (24 bytes)
247// ============================================================================
248
249/// Order status account - tracks partial fills and cancellations
250///
251/// Layout:
252/// - [0..8]   discriminator (8 bytes)
253/// - [8..16]  remaining (8 bytes)
254/// - [16]     is_cancelled (1 byte)
255/// - [17..24] _padding (7 bytes)
256#[derive(Debug, Clone)]
257pub struct OrderStatus {
258    /// Account discriminator
259    pub discriminator: [u8; 8],
260    /// Remaining maker_amount to be filled
261    pub remaining: u64,
262    /// Whether the order has been cancelled
263    pub is_cancelled: bool,
264}
265
266impl OrderStatus {
267    /// Account size in bytes
268    pub const LEN: usize = ORDER_STATUS_SIZE;
269
270    /// Deserialize from account data
271    pub fn deserialize(data: &[u8]) -> SdkResult<Self> {
272        if data.len() < Self::LEN {
273            return Err(SdkError::InvalidDataLength {
274                expected: Self::LEN,
275                actual: data.len(),
276            });
277        }
278
279        let discriminator = read_bytes::<8>(data, 0);
280        if discriminator != ORDER_STATUS_DISCRIMINATOR {
281            return Err(SdkError::InvalidDiscriminator {
282                expected: String::from_utf8_lossy(&ORDER_STATUS_DISCRIMINATOR).to_string(),
283                actual: String::from_utf8_lossy(&discriminator).to_string(),
284            });
285        }
286
287        Ok(Self {
288            discriminator,
289            remaining: read_u64(data, 8),
290            is_cancelled: data[16] != 0,
291        })
292    }
293
294    /// Check if account data has the order status discriminator
295    pub fn is_order_status_account(data: &[u8]) -> bool {
296        data.len() >= 8 && data[0..8] == ORDER_STATUS_DISCRIMINATOR
297    }
298}
299
300// ============================================================================
301// UserNonce Account (16 bytes)
302// ============================================================================
303
304/// User nonce account - tracks user's current nonce for mass cancellation
305///
306/// Layout:
307/// - [0..8]  discriminator (8 bytes)
308/// - [8..16] nonce (8 bytes)
309#[derive(Debug, Clone)]
310pub struct UserNonce {
311    /// Account discriminator
312    pub discriminator: [u8; 8],
313    /// Current nonce value
314    pub nonce: u64,
315}
316
317impl UserNonce {
318    /// Account size in bytes
319    pub const LEN: usize = USER_NONCE_SIZE;
320
321    /// Deserialize from account data
322    pub fn deserialize(data: &[u8]) -> SdkResult<Self> {
323        if data.len() < Self::LEN {
324            return Err(SdkError::InvalidDataLength {
325                expected: Self::LEN,
326                actual: data.len(),
327            });
328        }
329
330        let discriminator = read_bytes::<8>(data, 0);
331        if discriminator != USER_NONCE_DISCRIMINATOR {
332            return Err(SdkError::InvalidDiscriminator {
333                expected: String::from_utf8_lossy(&USER_NONCE_DISCRIMINATOR).to_string(),
334                actual: String::from_utf8_lossy(&discriminator).to_string(),
335            });
336        }
337
338        Ok(Self {
339            discriminator,
340            nonce: read_u64(data, 8),
341        })
342    }
343
344    /// Check if account data has the user nonce discriminator
345    pub fn is_user_nonce_account(data: &[u8]) -> bool {
346        data.len() >= 8 && data[0..8] == USER_NONCE_DISCRIMINATOR
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn test_exchange_deserialization() {
356        let mut data = vec![0u8; EXCHANGE_SIZE];
357        data[0..8].copy_from_slice(&EXCHANGE_DISCRIMINATOR);
358        // authority at offset 8
359        data[8..40].copy_from_slice(&[1u8; 32]);
360        // operator at offset 40
361        data[40..72].copy_from_slice(&[2u8; 32]);
362        // market_count at offset 72
363        data[72..80].copy_from_slice(&5u64.to_le_bytes());
364        // paused at offset 80
365        data[80] = 0;
366        // bump at offset 81
367        data[81] = 255;
368
369        let exchange = Exchange::deserialize(&data).unwrap();
370        assert_eq!(exchange.market_count, 5);
371        assert!(!exchange.paused);
372        assert_eq!(exchange.bump, 255);
373    }
374
375    #[test]
376    fn test_market_deserialization() {
377        let mut data = vec![0u8; MARKET_SIZE];
378        data[0..8].copy_from_slice(&MARKET_DISCRIMINATOR);
379        // market_id at offset 8
380        data[8..16].copy_from_slice(&42u64.to_le_bytes());
381        // num_outcomes at offset 16
382        data[16] = 3;
383        // status at offset 17
384        data[17] = 1; // Active
385        // winning_outcome at offset 18
386        data[18] = 255;
387        // has_winning_outcome at offset 19
388        data[19] = 0;
389        // bump at offset 20
390        data[20] = 254;
391
392        let market = Market::deserialize(&data).unwrap();
393        assert_eq!(market.market_id, 42);
394        assert_eq!(market.num_outcomes, 3);
395        assert_eq!(market.status, MarketStatus::Active);
396        assert_eq!(market.winning_outcome, 255);
397        assert!(!market.has_winning_outcome);
398    }
399
400    #[test]
401    fn test_position_deserialization() {
402        let mut data = vec![0u8; POSITION_SIZE];
403        data[0..8].copy_from_slice(&POSITION_DISCRIMINATOR);
404        // owner at offset 8
405        data[8..40].copy_from_slice(&[1u8; 32]);
406        // market at offset 40
407        data[40..72].copy_from_slice(&[2u8; 32]);
408        // bump at offset 72
409        data[72] = 253;
410
411        let position = Position::deserialize(&data).unwrap();
412        assert_eq!(position.bump, 253);
413    }
414
415    #[test]
416    fn test_order_status_deserialization() {
417        let mut data = vec![0u8; ORDER_STATUS_SIZE];
418        data[0..8].copy_from_slice(&ORDER_STATUS_DISCRIMINATOR);
419        // remaining at offset 8
420        data[8..16].copy_from_slice(&1000u64.to_le_bytes());
421        // is_cancelled at offset 16
422        data[16] = 0;
423
424        let order_status = OrderStatus::deserialize(&data).unwrap();
425        assert_eq!(order_status.remaining, 1000);
426        assert!(!order_status.is_cancelled);
427    }
428
429    #[test]
430    fn test_user_nonce_deserialization() {
431        let mut data = vec![0u8; USER_NONCE_SIZE];
432        data[0..8].copy_from_slice(&USER_NONCE_DISCRIMINATOR);
433        // nonce at offset 8
434        data[8..16].copy_from_slice(&99u64.to_le_bytes());
435
436        let user_nonce = UserNonce::deserialize(&data).unwrap();
437        assert_eq!(user_nonce.nonce, 99);
438    }
439}