light_client/interface/
account_interface.rs1use light_token::utils::get_associated_token_address_and_bump;
12use light_token_interface::state::ExtensionStruct;
13use solana_account::Account;
14use solana_pubkey::Pubkey;
15use spl_pod::{
16 bytemuck::{pod_bytes_of, pod_from_bytes, pod_get_packed_len},
17 primitives::PodU64,
18};
19use spl_token_2022_interface::{
20 pod::{PodAccount, PodCOption},
21 state::AccountState,
22};
23use thiserror::Error;
24
25use crate::indexer::{CompressedAccount, CompressedTokenAccount, TreeInfo};
26
27#[derive(Debug, Error)]
29pub enum AccountInterfaceError {
30 #[error("Account not found")]
31 NotFound,
32
33 #[error("Invalid account data")]
34 InvalidData,
35
36 #[error("Parse error: {0}")]
37 ParseError(String),
38}
39
40#[derive(Debug, Clone, PartialEq, Default)]
46pub struct AccountInterface {
47 pub key: Pubkey,
49 pub account: Account,
51 pub cold: Option<CompressedAccount>,
53}
54
55impl AccountInterface {
56 pub fn hot(key: Pubkey, account: Account) -> Self {
58 Self {
59 key,
60 account,
61 cold: None,
62 }
63 }
64
65 pub fn cold(key: Pubkey, compressed: CompressedAccount, owner: Pubkey) -> Self {
67 let data = compressed
68 .data
69 .as_ref()
70 .map(|d| {
71 let mut buf = d.discriminator.to_vec();
72 buf.extend_from_slice(&d.data);
73 buf
74 })
75 .unwrap_or_default();
76
77 Self {
78 key,
79 account: Account {
80 lamports: compressed.lamports,
81 data,
82 owner,
83 executable: false,
84 rent_epoch: 0,
85 },
86 cold: Some(compressed),
87 }
88 }
89
90 #[inline]
92 pub fn is_cold(&self) -> bool {
93 self.cold.is_some()
94 }
95
96 #[inline]
98 pub fn is_hot(&self) -> bool {
99 self.cold.is_none()
100 }
101
102 #[inline]
104 pub fn data(&self) -> &[u8] {
105 &self.account.data
106 }
107
108 pub fn hash(&self) -> Option<[u8; 32]> {
110 self.cold.as_ref().map(|c| c.hash)
111 }
112
113 pub fn tree_info(&self) -> Option<&TreeInfo> {
115 self.cold.as_ref().map(|c| &c.tree_info)
116 }
117
118 pub fn leaf_index(&self) -> Option<u32> {
120 self.cold.as_ref().map(|c| c.leaf_index)
121 }
122
123 pub fn as_compressed_account(&self) -> Option<&CompressedAccount> {
125 self.cold.as_ref()
126 }
127
128 pub fn as_mint(&self) -> Option<light_token_interface::state::Mint> {
130 let ca = self.cold.as_ref()?;
131 let data = ca.data.as_ref()?;
132 borsh::BorshDeserialize::deserialize(&mut data.data.as_slice()).ok()
133 }
134
135 pub fn mint_signer(&self) -> Option<[u8; 32]> {
137 self.as_mint().map(|m| m.metadata.mint_signer)
138 }
139
140 pub fn mint_compressed_address(&self) -> Option<[u8; 32]> {
142 self.as_mint().map(|m| m.metadata.compressed_address())
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Default)]
155pub struct TokenAccountInterface {
156 pub key: Pubkey,
158 pub account: Account,
160 pub parsed: PodAccount,
162 pub cold: Option<CompressedTokenAccount>,
164 pub extensions: Option<Vec<ExtensionStruct>>,
166}
167
168impl TokenAccountInterface {
169 pub fn hot(key: Pubkey, account: Account) -> Result<Self, AccountInterfaceError> {
171 let pod_len = pod_get_packed_len::<PodAccount>();
172 if account.data.len() < pod_len {
173 return Err(AccountInterfaceError::InvalidData);
174 }
175
176 let parsed: &PodAccount = pod_from_bytes(&account.data[..pod_len])
177 .map_err(|e| AccountInterfaceError::ParseError(e.to_string()))?;
178
179 Ok(Self {
180 key,
181 parsed: *parsed,
182 account,
183 cold: None,
184 extensions: None,
185 })
186 }
187
188 pub fn cold(
196 key: Pubkey,
197 compressed: CompressedTokenAccount,
198 owner_override: Pubkey,
199 program_owner: Pubkey,
200 ) -> Self {
201 use light_token::compat::AccountState as LightAccountState;
202
203 let token = &compressed.token;
204
205 let parsed = PodAccount {
206 mint: token.mint,
207 owner: owner_override,
208 amount: PodU64::from(token.amount),
209 delegate: match token.delegate {
210 Some(pk) => PodCOption::some(pk),
211 None => PodCOption::none(),
212 },
213 state: match token.state {
214 LightAccountState::Frozen => AccountState::Frozen as u8,
215 _ => AccountState::Initialized as u8,
216 },
217 is_native: PodCOption::none(),
218 delegated_amount: PodU64::from(0u64),
219 close_authority: PodCOption::none(),
220 };
221
222 let data = pod_bytes_of(&parsed).to_vec();
223
224 let extensions = token.tlv.clone();
225
226 let account = Account {
227 lamports: compressed.account.lamports,
228 data,
229 owner: program_owner,
230 executable: false,
231 rent_epoch: 0,
232 };
233
234 Self {
235 key,
236 account,
237 parsed,
238 cold: Some(compressed),
239 extensions,
240 }
241 }
242
243 #[inline]
245 pub fn is_cold(&self) -> bool {
246 self.cold.is_some()
247 }
248
249 #[inline]
251 pub fn is_hot(&self) -> bool {
252 self.cold.is_none()
253 }
254
255 pub fn compressed(&self) -> Option<&CompressedTokenAccount> {
257 self.cold.as_ref()
258 }
259
260 #[inline]
262 pub fn amount(&self) -> u64 {
263 u64::from(self.parsed.amount)
264 }
265
266 #[inline]
268 pub fn delegate(&self) -> Option<Pubkey> {
269 if self.parsed.delegate.is_some() {
270 Some(self.parsed.delegate.value)
271 } else {
272 None
273 }
274 }
275
276 #[inline]
278 pub fn mint(&self) -> Pubkey {
279 self.parsed.mint
280 }
281
282 #[inline]
284 pub fn owner(&self) -> Pubkey {
285 self.parsed.owner
286 }
287
288 #[inline]
290 pub fn is_frozen(&self) -> bool {
291 self.parsed.state == AccountState::Frozen as u8
292 }
293
294 #[inline]
296 pub fn hash(&self) -> Option<[u8; 32]> {
297 self.compressed().map(|c| c.account.hash)
298 }
299
300 #[inline]
302 pub fn tree_info(&self) -> Option<&TreeInfo> {
303 self.compressed().map(|c| &c.account.tree_info)
304 }
305
306 #[inline]
308 pub fn leaf_index(&self) -> Option<u32> {
309 self.compressed().map(|c| c.account.leaf_index)
310 }
311
312 pub fn ata_bump(&self) -> Option<u8> {
314 let (derived_ata, bump) =
315 get_associated_token_address_and_bump(&self.parsed.owner, &self.parsed.mint);
316 (derived_ata == self.key).then_some(bump)
317 }
318
319 pub fn is_ata(&self) -> bool {
321 self.ata_bump().is_some()
322 }
323}
324
325impl From<TokenAccountInterface> for AccountInterface {
326 fn from(tai: TokenAccountInterface) -> Self {
327 Self {
328 key: tai.key,
329 account: tai.account,
330 cold: tai.cold.map(|ct| ct.account),
331 }
332 }
333}