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