Skip to main content

light_client/indexer/types/
interface.rs

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