light_client/interface/
account_interface_ext.rs

1use async_trait::async_trait;
2use borsh::BorshDeserialize as _;
3use light_compressed_account::address::derive_address;
4use light_token::instruction::derive_token_ata;
5use light_token_interface::{state::Mint, MINT_ADDRESS_TREE};
6use solana_pubkey::Pubkey;
7
8use super::{AccountInterface, AccountToFetch, MintInterface, MintState, TokenAccountInterface};
9use crate::{
10    indexer::{GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer},
11    rpc::{Rpc, RpcError},
12};
13
14fn indexer_err(e: impl std::fmt::Display) -> RpcError {
15    RpcError::CustomError(format!("IndexerError: {}", e))
16}
17
18/// Extension trait for fetching account interfaces (unified hot/cold handling).
19#[async_trait]
20pub trait AccountInterfaceExt: Rpc + Indexer {
21    /// Fetch MintInterface for a mint account.
22    ///
23    /// Use this instead of get_account + unpack_mint.
24    async fn get_mint_interface(&self, address: &Pubkey) -> Result<MintInterface, RpcError>;
25
26    /// Fetch AccountInterface for an account.
27    ///
28    /// Use this instead of get_account.
29    async fn get_account_interface(
30        &self,
31        address: &Pubkey,
32        program_id: &Pubkey,
33    ) -> Result<AccountInterface, RpcError>;
34
35    /// Fetch TokenAccountInterface for a token account.
36    ///
37    /// Use this instead of get_token_account.
38    async fn get_token_account_interface(
39        &self,
40        address: &Pubkey,
41    ) -> Result<TokenAccountInterface, RpcError>;
42
43    /// Fetch TokenAccountInterface for an associated token account.
44    ///
45    /// Use this for all ATAs.
46    async fn get_ata_interface(
47        &self,
48        owner: &Pubkey,
49        mint: &Pubkey,
50    ) -> Result<TokenAccountInterface, RpcError>;
51
52    /// Fetch multiple accounts with automatic type dispatch.
53    ///
54    /// Use this instead of get_multiple_accounts.
55    async fn get_multiple_account_interfaces(
56        &self,
57        accounts: &[AccountToFetch],
58    ) -> Result<Vec<AccountInterface>, RpcError>;
59}
60
61// TODO: move all these to native RPC methods with single roundtrip.
62#[async_trait]
63impl<T: Rpc + Indexer> AccountInterfaceExt for T {
64    async fn get_mint_interface(&self, address: &Pubkey) -> Result<MintInterface, RpcError> {
65        let address_tree = Pubkey::new_from_array(MINT_ADDRESS_TREE);
66        let compressed_address = derive_address(
67            &address.to_bytes(),
68            &address_tree.to_bytes(),
69            &light_token_interface::LIGHT_TOKEN_PROGRAM_ID,
70        );
71
72        // Hot
73        if let Some(account) = self.get_account(*address).await? {
74            if account.lamports > 0 {
75                return Ok(MintInterface {
76                    mint: *address,
77                    address_tree,
78                    compressed_address,
79                    state: MintState::Hot { account },
80                });
81            }
82        }
83
84        // Cold
85        let result = self
86            .get_compressed_account(compressed_address, None)
87            .await
88            .map_err(indexer_err)?;
89
90        if let Some(compressed) = result.value {
91            if let Some(data) = compressed.data.as_ref() {
92                if !data.data.is_empty() {
93                    let mint_data = Mint::try_from_slice(&data.data)
94                        .map_err(|e| RpcError::CustomError(format!("mint parse error: {}", e)))?;
95                    return Ok(MintInterface {
96                        mint: *address,
97                        address_tree,
98                        compressed_address,
99                        state: MintState::Cold {
100                            compressed,
101                            mint_data,
102                        },
103                    });
104                }
105            }
106        }
107
108        Ok(MintInterface {
109            mint: *address,
110            address_tree,
111            compressed_address,
112            state: MintState::None,
113        })
114    }
115
116    async fn get_account_interface(
117        &self,
118        address: &Pubkey,
119        program_id: &Pubkey,
120    ) -> Result<AccountInterface, RpcError> {
121        let address_tree = self.get_address_tree_v2().tree;
122        let compressed_address = derive_address(
123            &address.to_bytes(),
124            &address_tree.to_bytes(),
125            &program_id.to_bytes(),
126        );
127
128        // Hot
129        if let Some(account) = self.get_account(*address).await? {
130            if account.lamports > 0 {
131                return Ok(AccountInterface::hot(*address, account));
132            }
133        }
134
135        // Cold
136        let result = self
137            .get_compressed_account(compressed_address, None)
138            .await
139            .map_err(indexer_err)?;
140
141        if let Some(compressed) = result.value {
142            if compressed.data.as_ref().is_some_and(|d| !d.data.is_empty()) {
143                return Ok(AccountInterface::cold(*address, compressed, *program_id));
144            }
145        }
146
147        // Doesn't exist.
148        let account = solana_account::Account {
149            lamports: 0,
150            data: vec![],
151            owner: *program_id,
152            executable: false,
153            rent_epoch: 0,
154        };
155        Ok(AccountInterface::hot(*address, account))
156    }
157
158    async fn get_token_account_interface(
159        &self,
160        address: &Pubkey,
161    ) -> Result<TokenAccountInterface, RpcError> {
162        use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID;
163
164        // Hot
165        if let Some(account) = self.get_account(*address).await? {
166            if account.lamports > 0 {
167                return TokenAccountInterface::hot(*address, account)
168                    .map_err(|e| RpcError::CustomError(format!("parse error: {}", e)));
169            }
170        }
171
172        // Cold (program-owned tokens: address = owner)
173        let result = self
174            .get_compressed_token_accounts_by_owner(address, None, None)
175            .await
176            .map_err(indexer_err)?;
177
178        if let Some(compressed) = result.value.items.into_iter().next() {
179            return Ok(TokenAccountInterface::cold(
180                *address,
181                compressed,
182                *address, // owner = hot address
183                LIGHT_TOKEN_PROGRAM_ID.into(),
184            ));
185        }
186
187        Err(RpcError::CustomError(format!(
188            "token account not found: {}",
189            address
190        )))
191    }
192
193    async fn get_ata_interface(
194        &self,
195        owner: &Pubkey,
196        mint: &Pubkey,
197    ) -> Result<TokenAccountInterface, RpcError> {
198        use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID;
199
200        let (ata, _bump) = derive_token_ata(owner, mint);
201
202        // Hot
203        if let Some(account) = self.get_account(ata).await? {
204            if account.lamports > 0 {
205                return TokenAccountInterface::hot(ata, account)
206                    .map_err(|e| RpcError::CustomError(format!("parse error: {}", e)));
207            }
208        }
209
210        // Cold (ATA query by address)
211        let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new(
212            Some(*mint),
213        ));
214        let result = self
215            .get_compressed_token_accounts_by_owner(&ata, options, None)
216            .await
217            .map_err(indexer_err)?;
218
219        if let Some(compressed) = result.value.items.into_iter().next() {
220            return Ok(TokenAccountInterface::cold(
221                ata,
222                compressed,
223                *owner, // owner_override = wallet owner
224                LIGHT_TOKEN_PROGRAM_ID.into(),
225            ));
226        }
227
228        Err(RpcError::CustomError(format!(
229            "ATA not found: owner={} mint={}",
230            owner, mint
231        )))
232    }
233
234    async fn get_multiple_account_interfaces(
235        &self,
236        accounts: &[AccountToFetch],
237    ) -> Result<Vec<AccountInterface>, RpcError> {
238        // TODO: concurrent with futures
239        let mut result = Vec::with_capacity(accounts.len());
240
241        for account in accounts {
242            let iface = match account {
243                AccountToFetch::Pda {
244                    address,
245                    program_id,
246                } => self.get_account_interface(address, program_id).await?,
247                AccountToFetch::Token { address } => {
248                    let token_iface = self.get_token_account_interface(address).await?;
249                    AccountInterface {
250                        key: token_iface.key,
251                        account: token_iface.account,
252                        cold: token_iface.cold,
253                    }
254                }
255                AccountToFetch::Ata { wallet_owner, mint } => {
256                    let token_iface = self.get_ata_interface(wallet_owner, mint).await?;
257                    AccountInterface {
258                        key: token_iface.key,
259                        account: token_iface.account,
260                        cold: token_iface.cold,
261                    }
262                }
263                AccountToFetch::Mint { address } => {
264                    let mint_iface = self.get_mint_interface(address).await?;
265                    match mint_iface.state {
266                        MintState::Hot { account } => AccountInterface {
267                            key: mint_iface.mint,
268                            account,
269                            cold: None,
270                        },
271                        MintState::Cold { compressed, .. } => {
272                            let owner = compressed.owner;
273                            AccountInterface::cold(mint_iface.mint, compressed, owner)
274                        }
275                        MintState::None => AccountInterface {
276                            key: mint_iface.mint,
277                            account: Default::default(),
278                            cold: None,
279                        },
280                    }
281                }
282            };
283            result.push(iface);
284        }
285
286        Ok(result)
287    }
288}