1use light_token::instruction::derive_token_ata;
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 super::ColdContext;
26use crate::indexer::{CompressedAccount, CompressedTokenAccount, TreeInfo};
27
28#[derive(Debug, Error)]
30pub enum AccountInterfaceError {
31 #[error("Account not found")]
32 NotFound,
33
34 #[error("Invalid account data")]
35 InvalidData,
36
37 #[error("Parse error: {0}")]
38 ParseError(String),
39}
40
41#[derive(Debug, Clone)]
47pub struct AccountInterface {
48 pub key: Pubkey,
50 pub account: Account,
52 pub cold: Option<ColdContext>,
54}
55
56impl AccountInterface {
57 pub fn hot(key: Pubkey, account: Account) -> Self {
59 Self {
60 key,
61 account,
62 cold: None,
63 }
64 }
65
66 pub fn cold(key: Pubkey, compressed: CompressedAccount, owner: Pubkey) -> Self {
68 let data = compressed
69 .data
70 .as_ref()
71 .map(|d| {
72 let mut buf = d.discriminator.to_vec();
73 buf.extend_from_slice(&d.data);
74 buf
75 })
76 .unwrap_or_default();
77
78 Self {
79 key,
80 account: Account {
81 lamports: compressed.lamports,
82 data,
83 owner,
84 executable: false,
85 rent_epoch: 0,
86 },
87 cold: Some(ColdContext::Account(compressed)),
88 }
89 }
90
91 pub fn cold_token(
93 key: Pubkey,
94 compressed: CompressedTokenAccount,
95 wallet_owner: Pubkey,
96 ) -> Self {
97 use light_token::compat::AccountState as LightAccountState;
98 let token = &compressed.token;
99 let parsed = PodAccount {
100 mint: token.mint,
101 owner: wallet_owner,
102 amount: PodU64::from(token.amount),
103 delegate: match token.delegate {
104 Some(pk) => PodCOption::some(pk),
105 None => PodCOption::none(),
106 },
107 state: match token.state {
108 LightAccountState::Frozen => AccountState::Frozen as u8,
109 _ => AccountState::Initialized as u8,
110 },
111 is_native: PodCOption::none(),
112 delegated_amount: PodU64::from(0u64),
113 close_authority: PodCOption::none(),
114 };
115 let data = pod_bytes_of(&parsed).to_vec();
116
117 Self {
118 key,
119 account: Account {
120 lamports: compressed.account.lamports,
121 data,
122 owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID,
123 executable: false,
124 rent_epoch: 0,
125 },
126 cold: Some(ColdContext::Token(compressed)),
127 }
128 }
129
130 #[inline]
132 pub fn is_cold(&self) -> bool {
133 self.cold.is_some()
134 }
135
136 #[inline]
138 pub fn is_hot(&self) -> bool {
139 self.cold.is_none()
140 }
141
142 #[inline]
144 pub fn data(&self) -> &[u8] {
145 &self.account.data
146 }
147
148 pub fn hash(&self) -> Option<[u8; 32]> {
150 match &self.cold {
151 Some(ColdContext::Account(c)) => Some(c.hash),
152 Some(ColdContext::Token(c)) => Some(c.account.hash),
153 None => None,
154 }
155 }
156
157 pub fn tree_info(&self) -> Option<&TreeInfo> {
159 match &self.cold {
160 Some(ColdContext::Account(c)) => Some(&c.tree_info),
161 Some(ColdContext::Token(c)) => Some(&c.account.tree_info),
162 None => None,
163 }
164 }
165
166 pub fn leaf_index(&self) -> Option<u32> {
168 match &self.cold {
169 Some(ColdContext::Account(c)) => Some(c.leaf_index),
170 Some(ColdContext::Token(c)) => Some(c.account.leaf_index),
171 None => None,
172 }
173 }
174
175 pub fn as_compressed_account(&self) -> Option<&CompressedAccount> {
177 match &self.cold {
178 Some(ColdContext::Account(c)) => Some(c),
179 _ => None,
180 }
181 }
182
183 pub fn as_compressed_token(&self) -> Option<&CompressedTokenAccount> {
185 match &self.cold {
186 Some(ColdContext::Token(c)) => Some(c),
187 _ => None,
188 }
189 }
190
191 pub fn as_mint(&self) -> Option<light_token_interface::state::Mint> {
193 match &self.cold {
194 Some(ColdContext::Account(ca)) => {
195 let data = ca.data.as_ref()?;
196 borsh::BorshDeserialize::deserialize(&mut data.data.as_slice()).ok()
197 }
198 _ => None,
199 }
200 }
201
202 pub fn mint_signer(&self) -> Option<[u8; 32]> {
204 self.as_mint().map(|m| m.metadata.mint_signer)
205 }
206
207 pub fn mint_compressed_address(&self) -> Option<[u8; 32]> {
209 self.as_mint().map(|m| m.metadata.compressed_address())
210 }
211}
212
213#[derive(Debug, Clone)]
222pub struct TokenAccountInterface {
223 pub key: Pubkey,
225 pub account: Account,
227 pub parsed: PodAccount,
229 pub cold: Option<ColdContext>,
231 pub extensions: Option<Vec<ExtensionStruct>>,
233}
234
235impl TokenAccountInterface {
236 pub fn hot(key: Pubkey, account: Account) -> Result<Self, AccountInterfaceError> {
238 let pod_len = pod_get_packed_len::<PodAccount>();
239 if account.data.len() < pod_len {
240 return Err(AccountInterfaceError::InvalidData);
241 }
242
243 let parsed: &PodAccount = pod_from_bytes(&account.data[..pod_len])
244 .map_err(|e| AccountInterfaceError::ParseError(e.to_string()))?;
245
246 Ok(Self {
247 key,
248 parsed: *parsed,
249 account,
250 cold: None,
251 extensions: None,
252 })
253 }
254
255 pub fn cold(
263 key: Pubkey,
264 compressed: CompressedTokenAccount,
265 owner_override: Pubkey,
266 program_owner: Pubkey,
267 ) -> Self {
268 use light_token::compat::AccountState as LightAccountState;
269
270 let token = &compressed.token;
271
272 let parsed = PodAccount {
273 mint: token.mint,
274 owner: owner_override,
275 amount: PodU64::from(token.amount),
276 delegate: match token.delegate {
277 Some(pk) => PodCOption::some(pk),
278 None => PodCOption::none(),
279 },
280 state: match token.state {
281 LightAccountState::Frozen => AccountState::Frozen as u8,
282 _ => AccountState::Initialized as u8,
283 },
284 is_native: PodCOption::none(),
285 delegated_amount: PodU64::from(0u64),
286 close_authority: PodCOption::none(),
287 };
288
289 let data = pod_bytes_of(&parsed).to_vec();
290
291 let extensions = token.tlv.clone();
292
293 let account = Account {
294 lamports: compressed.account.lamports,
295 data,
296 owner: program_owner,
297 executable: false,
298 rent_epoch: 0,
299 };
300
301 Self {
302 key,
303 account,
304 parsed,
305 cold: Some(ColdContext::Token(compressed)),
306 extensions,
307 }
308 }
309
310 #[inline]
312 pub fn is_cold(&self) -> bool {
313 self.cold.is_some()
314 }
315
316 #[inline]
318 pub fn is_hot(&self) -> bool {
319 self.cold.is_none()
320 }
321
322 pub fn compressed(&self) -> Option<&CompressedTokenAccount> {
324 match &self.cold {
325 Some(ColdContext::Token(c)) => Some(c),
326 _ => None,
327 }
328 }
329
330 #[inline]
332 pub fn amount(&self) -> u64 {
333 u64::from(self.parsed.amount)
334 }
335
336 #[inline]
338 pub fn delegate(&self) -> Option<Pubkey> {
339 if self.parsed.delegate.is_some() {
340 Some(self.parsed.delegate.value)
341 } else {
342 None
343 }
344 }
345
346 #[inline]
348 pub fn mint(&self) -> Pubkey {
349 self.parsed.mint
350 }
351
352 #[inline]
354 pub fn owner(&self) -> Pubkey {
355 self.parsed.owner
356 }
357
358 #[inline]
360 pub fn is_frozen(&self) -> bool {
361 self.parsed.state == AccountState::Frozen as u8
362 }
363
364 #[inline]
366 pub fn hash(&self) -> Option<[u8; 32]> {
367 self.compressed().map(|c| c.account.hash)
368 }
369
370 #[inline]
372 pub fn tree_info(&self) -> Option<&TreeInfo> {
373 self.compressed().map(|c| &c.account.tree_info)
374 }
375
376 #[inline]
378 pub fn leaf_index(&self) -> Option<u32> {
379 self.compressed().map(|c| c.account.leaf_index)
380 }
381
382 pub fn ata_bump(&self) -> Option<u8> {
384 let (derived_ata, bump) = derive_token_ata(&self.parsed.owner, &self.parsed.mint);
385 (derived_ata == self.key).then_some(bump)
386 }
387
388 pub fn is_ata(&self) -> bool {
390 self.ata_bump().is_some()
391 }
392}