Skip to main content

light_client/interface/
light_program_interface.rs

1//! LightProgramInterface trait and supporting types for client-side SDK patterns.
2//!
3//! Core types:
4//! - `ColdContext` - Cold data context (Account or Token)
5//! - `PdaSpec` - Spec for PDA loading with typed variant
6//! - `AccountSpec` - Unified spec enum for load instruction building
7//! - `LightProgramInterface` - Trait for program SDKs
8
9use std::fmt::Debug;
10
11use light_account::Pack;
12use light_token::instruction::derive_token_ata;
13use solana_pubkey::Pubkey;
14
15use super::{AccountInterface, TokenAccountInterface};
16use crate::indexer::{CompressedAccount, CompressedTokenAccount};
17
18/// Account descriptor for fetching. Routes to the correct indexer endpoint.
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum AccountToFetch {
21    /// PDA account - uses `get_account_interface(address, program_id)`
22    Pda { address: Pubkey, program_id: Pubkey },
23    /// Token account (program-owned) - uses `get_token_account_interface(address)`
24    Token { address: Pubkey },
25    /// ATA - uses `get_ata_interface(wallet_owner, mint)`
26    Ata { wallet_owner: Pubkey, mint: Pubkey },
27    /// Light mint - uses `get_mint_interface(address)`
28    Mint { address: Pubkey },
29}
30
31impl AccountToFetch {
32    pub fn pda(address: Pubkey, program_id: Pubkey) -> Self {
33        Self::Pda {
34            address,
35            program_id,
36        }
37    }
38
39    pub fn token(address: Pubkey) -> Self {
40        Self::Token { address }
41    }
42
43    pub fn ata(wallet_owner: Pubkey, mint: Pubkey) -> Self {
44        Self::Ata { wallet_owner, mint }
45    }
46
47    pub fn mint(address: Pubkey) -> Self {
48        Self::Mint { address }
49    }
50
51    #[must_use]
52    pub fn pubkey(&self) -> Pubkey {
53        match self {
54            Self::Pda { address, .. } => *address,
55            Self::Token { address } => *address,
56            Self::Ata { wallet_owner, mint } => derive_token_ata(wallet_owner, mint).0,
57            Self::Mint { address } => *address,
58        }
59    }
60}
61
62/// Context for cold accounts.
63///
64/// Two variants based on data structure, not account type:
65/// - `Account` - PDA
66/// - `Token` - Token account
67#[derive(Clone, Debug)]
68pub enum ColdContext {
69    /// PDA
70    Account(CompressedAccount),
71    /// Token account
72    Token(CompressedTokenAccount),
73}
74
75/// Specification for a program-owned PDA with typed variant.
76///
77/// Embeds `AccountInterface` for account data and adds `variant` for typed variant.
78#[derive(Clone, Debug)]
79pub struct PdaSpec<V> {
80    /// The account interface.
81    pub interface: AccountInterface,
82    /// The typed variant with all seed values populated.
83    pub variant: V,
84    /// The program owner to call for loading the account.
85    pub program_id: Pubkey,
86}
87
88impl<V> PdaSpec<V> {
89    /// Create a new PdaSpec from an interface, variant, and program owner.
90    #[must_use]
91    pub fn new(interface: AccountInterface, variant: V, program_id: Pubkey) -> Self {
92        Self {
93            interface,
94            variant,
95            program_id,
96        }
97    }
98
99    /// The account's public key.
100    #[inline]
101    #[must_use]
102    pub fn address(&self) -> Pubkey {
103        self.interface.key
104    }
105
106    /// The program owner to call for loading the account.
107    #[inline]
108    #[must_use]
109    pub fn program_id(&self) -> Pubkey {
110        self.program_id
111    }
112
113    /// Whether this account is cold and must be loaded.
114    #[inline]
115    #[must_use]
116    pub fn is_cold(&self) -> bool {
117        self.interface.is_cold()
118    }
119
120    /// Whether this account is hot and will not be loaded.
121    #[inline]
122    #[must_use]
123    pub fn is_hot(&self) -> bool {
124        self.interface.is_hot()
125    }
126
127    /// Get the compressed account if cold (handles both Account and Token cold contexts).
128    #[must_use]
129    pub fn compressed(&self) -> Option<&CompressedAccount> {
130        match &self.interface.cold {
131            Some(ColdContext::Account(c)) => Some(c),
132            Some(ColdContext::Token(c)) => Some(&c.account),
133            None => None,
134        }
135    }
136
137    /// Get the compressed token account if this is a cold token PDA.
138    #[must_use]
139    pub fn compressed_token(&self) -> Option<&CompressedTokenAccount> {
140        self.interface.as_compressed_token()
141    }
142
143    /// Whether this spec is for a token PDA (cold context is Token variant).
144    #[must_use]
145    pub fn is_token_pda(&self) -> bool {
146        self.interface.as_compressed_token().is_some()
147    }
148
149    /// Get the cold account hash.
150    #[must_use]
151    pub fn hash(&self) -> Option<[u8; 32]> {
152        self.interface.hash()
153    }
154
155    /// Get account data bytes.
156    #[inline]
157    #[must_use]
158    pub fn data(&self) -> &[u8] {
159        self.interface.data()
160    }
161}
162
163/// Account specification for loading cold accounts.
164#[derive(Clone, Debug)]
165pub enum AccountSpec<V> {
166    /// Program-owned PDA with typed variant.
167    Pda(PdaSpec<V>),
168    /// Associated token account
169    Ata(TokenAccountInterface),
170    /// Light token mint
171    Mint(AccountInterface),
172}
173
174impl<V> AccountSpec<V> {
175    #[inline]
176    #[must_use]
177    pub fn is_cold(&self) -> bool {
178        match self {
179            Self::Pda(s) => s.is_cold(),
180            Self::Ata(s) => s.is_cold(),
181            Self::Mint(s) => s.is_cold(),
182        }
183    }
184
185    #[inline]
186    #[must_use]
187    pub fn is_hot(&self) -> bool {
188        !self.is_cold()
189    }
190
191    #[must_use]
192    pub fn pubkey(&self) -> Pubkey {
193        match self {
194            Self::Pda(s) => s.address(),
195            Self::Ata(s) => s.key,
196            Self::Mint(s) => s.key,
197        }
198    }
199}
200
201impl<V> From<PdaSpec<V>> for AccountSpec<V> {
202    fn from(spec: PdaSpec<V>) -> Self {
203        Self::Pda(spec)
204    }
205}
206
207impl From<TokenAccountInterface> for AccountSpec<()> {
208    fn from(interface: TokenAccountInterface) -> Self {
209        Self::Ata(interface)
210    }
211}
212
213impl From<AccountInterface> for AccountSpec<()> {
214    fn from(interface: AccountInterface) -> Self {
215        Self::Mint(interface)
216    }
217}
218
219/// Check if any specs in the slice are cold.
220#[inline]
221#[must_use]
222pub fn any_cold<V>(specs: &[AccountSpec<V>]) -> bool {
223    specs.iter().any(|s| s.is_cold())
224}
225
226/// Check if all specs in the slice are hot.
227#[inline]
228#[must_use]
229pub fn all_hot<V>(specs: &[AccountSpec<V>]) -> bool {
230    specs.iter().all(|s| s.is_hot())
231}
232
233/// Trait for programs to give clients a unified API to load cold program accounts.
234pub trait LightProgramInterface: Sized {
235    /// The program's interface account variant enum.
236    type Variant: Pack<solana_instruction::AccountMeta> + Clone + Debug;
237
238    /// Program-specific instruction enum.
239    type Instruction;
240
241    /// Error type for SDK operations.
242    type Error: std::error::Error;
243
244    /// The program ID.
245    #[must_use]
246    fn program_id(&self) -> Pubkey;
247
248    /// Construct SDK from root account(s).
249    fn from_keyed_accounts(accounts: &[AccountInterface]) -> Result<Self, Self::Error>;
250
251    /// Returns pubkeys of accounts needed for an instruction.
252    #[must_use]
253    fn get_accounts_to_update(&self, ix: &Self::Instruction) -> Vec<AccountToFetch>;
254
255    /// Update internal cache with fetched account data.
256    fn update(&mut self, accounts: &[AccountInterface]) -> Result<(), Self::Error>;
257
258    /// Get all cached specs.
259    #[must_use]
260    fn get_all_specs(&self) -> Vec<AccountSpec<Self::Variant>>;
261
262    /// Get specs filtered for a specific instruction.
263    #[must_use]
264    fn get_specs_for_instruction(&self, ix: &Self::Instruction) -> Vec<AccountSpec<Self::Variant>>;
265
266    /// Get only cold specs from all cached specs.
267    #[must_use]
268    fn get_cold_specs(&self) -> Vec<AccountSpec<Self::Variant>> {
269        self.get_all_specs()
270            .into_iter()
271            .filter(|s| s.is_cold())
272            .collect()
273    }
274
275    /// Get only cold specs for a specific instruction.
276    #[must_use]
277    fn get_cold_specs_for_instruction(
278        &self,
279        ix: &Self::Instruction,
280    ) -> Vec<AccountSpec<Self::Variant>> {
281        self.get_specs_for_instruction(ix)
282            .into_iter()
283            .filter(|s| s.is_cold())
284            .collect()
285    }
286
287    /// Check if any accounts for this instruction are cold.
288    #[must_use]
289    fn needs_loading(&self, ix: &Self::Instruction) -> bool {
290        any_cold(&self.get_specs_for_instruction(ix))
291    }
292}
293
294/// Extract 8-byte discriminator from account data.
295#[inline]
296#[must_use]
297pub fn discriminator(data: &[u8]) -> Option<[u8; 8]> {
298    data.get(..8).and_then(|s| s.try_into().ok())
299}
300
301/// Check if account data matches a discriminator.
302#[inline]
303#[must_use]
304pub fn matches_discriminator(data: &[u8], disc: &[u8; 8]) -> bool {
305    discriminator(data) == Some(*disc)
306}