Skip to main content

light_client/indexer/types/
interface.rs

1use light_compressed_account::TreeType;
2use light_token::compat::TokenData;
3use solana_account::Account;
4use solana_pubkey::Pubkey;
5
6use super::super::{base58::decode_base58_to_fixed_array, IndexerError};
7
8/// Re-export solana Account for interface types.
9pub type SolanaAccountData = Account;
10
11/// Merkle tree info for compressed accounts
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct InterfaceTreeInfo {
14    pub tree: Pubkey,
15    pub queue: Pubkey,
16    pub tree_type: TreeType,
17    pub seq: Option<u64>,
18    /// Slot when the account was created/compressed
19    pub slot_created: u64,
20}
21
22/// Structured compressed account data (discriminator separated)
23#[derive(Clone, Debug, PartialEq)]
24pub struct ColdData {
25    pub discriminator: [u8; 8],
26    pub data: Vec<u8>,
27    pub data_hash: [u8; 32],
28}
29
30/// Compressed account context — present when account is in compressed state.
31#[derive(Clone, Debug, PartialEq)]
32pub struct ColdContext {
33    pub hash: [u8; 32],
34    pub leaf_index: u64,
35    pub tree_info: InterfaceTreeInfo,
36    pub data: ColdData,
37    pub address: Option<[u8; 32]>,
38    pub prove_by_index: bool,
39}
40
41/// Decode tree info from photon_api AccountV2 format
42fn decode_tree_info_v2(
43    merkle_ctx: &photon_api::types::MerkleContextV2,
44    seq: Option<u64>,
45    slot_created: u64,
46) -> Result<InterfaceTreeInfo, IndexerError> {
47    let tree = Pubkey::new_from_array(decode_base58_to_fixed_array(&merkle_ctx.tree)?);
48    let queue = Pubkey::new_from_array(decode_base58_to_fixed_array(&merkle_ctx.queue)?);
49    let tree_type = TreeType::from(merkle_ctx.tree_type as u64);
50    Ok(InterfaceTreeInfo {
51        tree,
52        queue,
53        tree_type,
54        seq,
55        slot_created,
56    })
57}
58
59/// Decode cold data from photon_api AccountData format.
60fn decode_account_data(data: &photon_api::types::AccountData) -> Result<ColdData, IndexerError> {
61    let disc_val = *data.discriminator;
62    let discriminator = disc_val.to_le_bytes();
63    Ok(ColdData {
64        discriminator,
65        data: base64::decode_config(&*data.data, base64::STANDARD_NO_PAD)
66            .map_err(|e| IndexerError::decode_error("data", e))?,
67        data_hash: decode_base58_to_fixed_array(&data.data_hash)?,
68    })
69}
70
71/// Convert a photon_api AccountV2 to a client ColdContext.
72fn convert_account_v2(av2: &photon_api::types::AccountV2) -> Result<ColdContext, IndexerError> {
73    let tree_info = decode_tree_info_v2(
74        &av2.merkle_context,
75        av2.seq.as_ref().map(|s| **s),
76        *av2.slot_created,
77    )?;
78
79    let data = match &av2.data {
80        Some(d) => decode_account_data(d)?,
81        None => ColdData {
82            discriminator: [0u8; 8],
83            data: Vec::new(),
84            data_hash: [0u8; 32],
85        },
86    };
87
88    let address = av2
89        .address
90        .as_ref()
91        .map(|a| decode_base58_to_fixed_array(a))
92        .transpose()?;
93
94    Ok(ColdContext {
95        hash: decode_base58_to_fixed_array(&av2.hash)?,
96        leaf_index: *av2.leaf_index,
97        tree_info,
98        data,
99        address,
100        prove_by_index: av2.prove_by_index,
101    })
102}
103
104/// Unified account interface — works for both on-chain and compressed accounts
105#[derive(Clone, Debug, PartialEq)]
106pub struct AccountInterface {
107    /// The on-chain Solana pubkey
108    pub key: Pubkey,
109    /// Standard Solana account fields
110    pub account: SolanaAccountData,
111    /// Compressed context — None if on-chain, Some if compressed
112    pub cold: Option<ColdContext>,
113}
114
115impl AccountInterface {
116    /// Returns true if this account is on-chain (hot)
117    pub fn is_hot(&self) -> bool {
118        self.cold.is_none()
119    }
120
121    /// Returns true if this account is compressed (cold)
122    pub fn is_cold(&self) -> bool {
123        self.cold.is_some()
124    }
125}
126
127/// Helper to convert photon_api AccountInterface to client AccountInterface
128fn convert_account_interface(
129    ai: &photon_api::types::AccountInterface,
130) -> Result<AccountInterface, IndexerError> {
131    // Take the first compressed account entry if present
132    let cold = ai
133        .cold
134        .as_ref()
135        .and_then(|entries| entries.first())
136        .map(convert_account_v2)
137        .transpose()?;
138
139    let data = base64::decode_config(&*ai.account.data, base64::STANDARD_NO_PAD)
140        .map_err(|e| IndexerError::decode_error("account.data", e))?;
141
142    Ok(AccountInterface {
143        key: Pubkey::new_from_array(decode_base58_to_fixed_array(&ai.key)?),
144        account: Account {
145            lamports: *ai.account.lamports,
146            data,
147            owner: Pubkey::new_from_array(decode_base58_to_fixed_array(&ai.account.owner)?),
148            executable: ai.account.executable,
149            rent_epoch: *ai.account.rent_epoch,
150        },
151        cold,
152    })
153}
154
155impl TryFrom<&photon_api::types::AccountInterface> for AccountInterface {
156    type Error = IndexerError;
157
158    fn try_from(ai: &photon_api::types::AccountInterface) -> Result<Self, Self::Error> {
159        convert_account_interface(ai)
160    }
161}
162
163/// Token account interface with parsed token data
164#[derive(Clone, Debug, PartialEq)]
165pub struct TokenAccountInterface {
166    /// Base account interface data
167    pub account: AccountInterface,
168    /// Parsed token data (same as CompressedTokenAccount.token)
169    pub token: TokenData,
170}