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_sdk::interface::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.
128    #[must_use]
129    pub fn compressed(&self) -> Option<&CompressedAccount> {
130        self.interface.as_compressed_account()
131    }
132
133    /// Get the cold account hash.
134    #[must_use]
135    pub fn hash(&self) -> Option<[u8; 32]> {
136        self.interface.hash()
137    }
138
139    /// Get account data bytes.
140    #[inline]
141    #[must_use]
142    pub fn data(&self) -> &[u8] {
143        self.interface.data()
144    }
145}
146
147/// Account specification for loading cold accounts.
148#[derive(Clone, Debug)]
149pub enum AccountSpec<V> {
150    /// Program-owned PDA with typed variant.
151    Pda(PdaSpec<V>),
152    /// Associated token account
153    Ata(TokenAccountInterface),
154    /// Light token mint
155    Mint(AccountInterface),
156}
157
158impl<V> AccountSpec<V> {
159    #[inline]
160    #[must_use]
161    pub fn is_cold(&self) -> bool {
162        match self {
163            Self::Pda(s) => s.is_cold(),
164            Self::Ata(s) => s.is_cold(),
165            Self::Mint(s) => s.is_cold(),
166        }
167    }
168
169    #[inline]
170    #[must_use]
171    pub fn is_hot(&self) -> bool {
172        !self.is_cold()
173    }
174
175    #[must_use]
176    pub fn pubkey(&self) -> Pubkey {
177        match self {
178            Self::Pda(s) => s.address(),
179            Self::Ata(s) => s.key,
180            Self::Mint(s) => s.key,
181        }
182    }
183}
184
185impl<V> From<PdaSpec<V>> for AccountSpec<V> {
186    fn from(spec: PdaSpec<V>) -> Self {
187        Self::Pda(spec)
188    }
189}
190
191impl From<TokenAccountInterface> for AccountSpec<()> {
192    fn from(interface: TokenAccountInterface) -> Self {
193        Self::Ata(interface)
194    }
195}
196
197impl From<AccountInterface> for AccountSpec<()> {
198    fn from(interface: AccountInterface) -> Self {
199        Self::Mint(interface)
200    }
201}
202
203/// Check if any specs in the slice are cold.
204#[inline]
205#[must_use]
206pub fn any_cold<V>(specs: &[AccountSpec<V>]) -> bool {
207    specs.iter().any(|s| s.is_cold())
208}
209
210/// Check if all specs in the slice are hot.
211#[inline]
212#[must_use]
213pub fn all_hot<V>(specs: &[AccountSpec<V>]) -> bool {
214    specs.iter().all(|s| s.is_hot())
215}
216
217/// Trait for programs to give clients a unified API to load cold program accounts.
218pub trait LightProgramInterface: Sized {
219    /// The program's interface account variant enum.
220    type Variant: Pack + Clone + Debug;
221
222    /// Program-specific instruction enum.
223    type Instruction;
224
225    /// Error type for SDK operations.
226    type Error: std::error::Error;
227
228    /// The program ID.
229    #[must_use]
230    fn program_id(&self) -> Pubkey;
231
232    /// Construct SDK from root account(s).
233    fn from_keyed_accounts(accounts: &[AccountInterface]) -> Result<Self, Self::Error>;
234
235    /// Returns pubkeys of accounts needed for an instruction.
236    #[must_use]
237    fn get_accounts_to_update(&self, ix: &Self::Instruction) -> Vec<AccountToFetch>;
238
239    /// Update internal cache with fetched account data.
240    fn update(&mut self, accounts: &[AccountInterface]) -> Result<(), Self::Error>;
241
242    /// Get all cached specs.
243    #[must_use]
244    fn get_all_specs(&self) -> Vec<AccountSpec<Self::Variant>>;
245
246    /// Get specs filtered for a specific instruction.
247    #[must_use]
248    fn get_specs_for_instruction(&self, ix: &Self::Instruction) -> Vec<AccountSpec<Self::Variant>>;
249
250    /// Get only cold specs from all cached specs.
251    #[must_use]
252    fn get_cold_specs(&self) -> Vec<AccountSpec<Self::Variant>> {
253        self.get_all_specs()
254            .into_iter()
255            .filter(|s| s.is_cold())
256            .collect()
257    }
258
259    /// Get only cold specs for a specific instruction.
260    #[must_use]
261    fn get_cold_specs_for_instruction(
262        &self,
263        ix: &Self::Instruction,
264    ) -> Vec<AccountSpec<Self::Variant>> {
265        self.get_specs_for_instruction(ix)
266            .into_iter()
267            .filter(|s| s.is_cold())
268            .collect()
269    }
270
271    /// Check if any accounts for this instruction are cold.
272    #[must_use]
273    fn needs_loading(&self, ix: &Self::Instruction) -> bool {
274        any_cold(&self.get_specs_for_instruction(ix))
275    }
276}
277
278/// Extract 8-byte discriminator from account data.
279#[inline]
280#[must_use]
281pub fn discriminator(data: &[u8]) -> Option<[u8; 8]> {
282    data.get(..8).and_then(|s| s.try_into().ok())
283}
284
285/// Check if account data matches a discriminator.
286#[inline]
287#[must_use]
288pub fn matches_discriminator(data: &[u8], disc: &[u8; 8]) -> bool {
289    discriminator(data) == Some(*disc)
290}