-
Notifications
You must be signed in to change notification settings - Fork 93
Expand file tree
/
Copy pathaccount_interface.rs
More file actions
331 lines (287 loc) · 9.45 KB
/
account_interface.rs
File metadata and controls
331 lines (287 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
//! Unified account interfaces for hot/cold account handling.
//!
//! Core types:
//! - `AccountInterface` - Generic account (PDAs, mints)
//! - `TokenAccountInterface` - Token accounts (ATAs, program-owned vaults)
//!
//! All interfaces use standard Solana/SPL types:
//! - `solana_account::Account` for raw account data
//! - `spl_token_2022_interface::pod::PodAccount` for parsed token data
use light_token::utils::get_associated_token_address_and_bump;
use light_token_interface::state::ExtensionStruct;
use solana_account::Account;
use solana_pubkey::Pubkey;
use spl_pod::{
bytemuck::{pod_bytes_of, pod_from_bytes, pod_get_packed_len},
primitives::PodU64,
};
use spl_token_2022_interface::{
pod::{PodAccount, PodCOption},
state::AccountState,
};
use thiserror::Error;
use crate::indexer::{CompressedAccount, CompressedTokenAccount, TreeInfo};
/// Error type for account interface operations.
#[derive(Debug, Error)]
pub enum AccountInterfaceError {
#[error("Account not found")]
NotFound,
#[error("Invalid account data")]
InvalidData,
#[error("Parse error: {0}")]
ParseError(String),
}
/// Unified account interface for PDAs, mints, and tokens.
///
/// Uses standard `solana_account::Account` for raw data.
/// For hot accounts: actual on-chain bytes.
/// For cold accounts: synthetic bytes from cold data.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct AccountInterface {
/// The account's public key.
pub key: Pubkey,
/// Standard Solana Account (lamports, data, owner, executable, rent_epoch).
pub account: Account,
/// Compressed account data (only present when cold).
pub cold: Option<CompressedAccount>,
}
impl AccountInterface {
/// Create a hot (on-chain) account interface.
pub fn hot(key: Pubkey, account: Account) -> Self {
Self {
key,
account,
cold: None,
}
}
/// Create a cold account interface for a PDA/mint.
///
/// `data.data` contains the full on-chain account bytes as-is (no reassembly needed).
pub fn cold(key: Pubkey, compressed: CompressedAccount, owner: Pubkey) -> Self {
let data = compressed
.data
.as_ref()
.map(|d| d.data.clone())
.unwrap_or_default();
Self {
key,
account: Account {
lamports: compressed.lamports,
data,
owner,
executable: false,
rent_epoch: 0,
},
cold: Some(compressed),
}
}
/// Whether this account is cold.
#[inline]
pub fn is_cold(&self) -> bool {
self.cold.is_some()
}
/// Whether this account is hot.
#[inline]
pub fn is_hot(&self) -> bool {
self.cold.is_none()
}
/// Get data bytes.
#[inline]
pub fn data(&self) -> &[u8] {
&self.account.data
}
/// Get the account hash if cold.
pub fn hash(&self) -> Option<[u8; 32]> {
self.cold.as_ref().map(|c| c.hash)
}
/// Get tree info if cold.
pub fn tree_info(&self) -> Option<&TreeInfo> {
self.cold.as_ref().map(|c| &c.tree_info)
}
/// Get leaf index if cold.
pub fn leaf_index(&self) -> Option<u32> {
self.cold.as_ref().map(|c| c.leaf_index)
}
/// Get as CompressedAccount if cold.
pub fn as_compressed_account(&self) -> Option<&CompressedAccount> {
self.cold.as_ref()
}
/// Try to parse as Mint. Returns None if not a mint or parse fails.
pub fn as_mint(&self) -> Option<light_token_interface::state::Mint> {
let ca = self.cold.as_ref()?;
let data = ca.data.as_ref()?;
borsh::BorshDeserialize::deserialize(&mut data.data.as_slice()).ok()
}
/// Get mint signer if this is a cold mint.
pub fn mint_signer(&self) -> Option<[u8; 32]> {
self.as_mint().map(|m| m.metadata.mint_signer)
}
/// Get mint compressed address if this is a cold mint.
pub fn mint_compressed_address(&self) -> Option<[u8; 32]> {
self.as_mint().map(|m| m.metadata.compressed_address())
}
}
/// Token account interface with both raw and parsed data.
///
/// Uses standard types:
/// - `solana_account::Account` for raw bytes
/// - `spl_token_2022_interface::pod::PodAccount` for parsed token data
///
/// For ATAs: `parsed.owner` is the wallet owner (set from fetch params).
/// For program-owned: `parsed.owner` is the PDA.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TokenAccountInterface {
/// The token account's public key.
pub key: Pubkey,
/// Standard Solana Account (lamports, data, owner, executable, rent_epoch).
pub account: Account,
/// Parsed SPL Token Account (POD format).
pub parsed: PodAccount,
/// Compressed token account data (only present when cold).
pub cold: Option<CompressedTokenAccount>,
/// Optional TLV extension data.
pub extensions: Option<Vec<ExtensionStruct>>,
}
impl TokenAccountInterface {
/// Create a hot (on-chain) token account interface.
pub fn hot(key: Pubkey, account: Account) -> Result<Self, AccountInterfaceError> {
let pod_len = pod_get_packed_len::<PodAccount>();
if account.data.len() < pod_len {
return Err(AccountInterfaceError::InvalidData);
}
let parsed: &PodAccount = pod_from_bytes(&account.data[..pod_len])
.map_err(|e| AccountInterfaceError::ParseError(e.to_string()))?;
Ok(Self {
key,
parsed: *parsed,
account,
cold: None,
extensions: None,
})
}
/// Create a cold token account interface.
///
/// # Arguments
/// * `key` - The token account address
/// * `compressed` - The cold token account from indexer
/// * `owner_override` - For ATAs, pass the wallet owner. For program-owned, pass the PDA.
/// * `program_owner` - The program that owns this account (usually LIGHT_TOKEN_PROGRAM_ID)
pub fn cold(
key: Pubkey,
compressed: CompressedTokenAccount,
owner_override: Pubkey,
program_owner: Pubkey,
) -> Self {
use light_token::compat::AccountState as LightAccountState;
let token = &compressed.token;
let parsed = PodAccount {
mint: token.mint,
owner: owner_override,
amount: PodU64::from(token.amount),
delegate: match token.delegate {
Some(pk) => PodCOption::some(pk),
None => PodCOption::none(),
},
state: match token.state {
LightAccountState::Frozen => AccountState::Frozen as u8,
_ => AccountState::Initialized as u8,
},
is_native: PodCOption::none(),
delegated_amount: PodU64::from(0u64),
close_authority: PodCOption::none(),
};
let data = pod_bytes_of(&parsed).to_vec();
let extensions = token.tlv.clone();
let account = Account {
lamports: compressed.account.lamports,
data,
owner: program_owner,
executable: false,
rent_epoch: 0,
};
Self {
key,
account,
parsed,
cold: Some(compressed),
extensions,
}
}
/// Whether this account is cold.
#[inline]
pub fn is_cold(&self) -> bool {
self.cold.is_some()
}
/// Whether this account is hot.
#[inline]
pub fn is_hot(&self) -> bool {
self.cold.is_none()
}
/// Get the CompressedTokenAccount if cold.
pub fn compressed(&self) -> Option<&CompressedTokenAccount> {
self.cold.as_ref()
}
/// Get amount.
#[inline]
pub fn amount(&self) -> u64 {
u64::from(self.parsed.amount)
}
/// Get delegate.
#[inline]
pub fn delegate(&self) -> Option<Pubkey> {
if self.parsed.delegate.is_some() {
Some(self.parsed.delegate.value)
} else {
None
}
}
/// Get mint.
#[inline]
pub fn mint(&self) -> Pubkey {
self.parsed.mint
}
/// Get owner (wallet for ATAs, PDA for program-owned).
#[inline]
pub fn owner(&self) -> Pubkey {
self.parsed.owner
}
/// Check if frozen.
#[inline]
pub fn is_frozen(&self) -> bool {
self.parsed.state == AccountState::Frozen as u8
}
/// Get the account hash if cold.
#[inline]
pub fn hash(&self) -> Option<[u8; 32]> {
self.compressed().map(|c| c.account.hash)
}
/// Get tree info if cold.
#[inline]
pub fn tree_info(&self) -> Option<&TreeInfo> {
self.compressed().map(|c| &c.account.tree_info)
}
/// Get leaf index if cold.
#[inline]
pub fn leaf_index(&self) -> Option<u32> {
self.compressed().map(|c| c.account.leaf_index)
}
/// Get ATA bump if this is an ATA. Returns None if not a valid ATA derivation.
pub fn ata_bump(&self) -> Option<u8> {
let (derived_ata, bump) =
get_associated_token_address_and_bump(&self.parsed.owner, &self.parsed.mint);
(derived_ata == self.key).then_some(bump)
}
/// Check if this token account is an ATA (derivation matches).
pub fn is_ata(&self) -> bool {
self.ata_bump().is_some()
}
}
impl From<TokenAccountInterface> for AccountInterface {
fn from(tai: TokenAccountInterface) -> Self {
Self {
key: tai.key,
account: tai.account,
cold: tai.cold.map(|ct| ct.account),
}
}
}