Skip to main content

klend_interface/helpers/
info.rs

1use solana_pubkey::Pubkey;
2
3use crate::state::AccountDataError;
4
5/// On-chain reserve data that cannot be derived from PDAs.
6///
7/// The caller reads these fields from the deserialized `Reserve` account.
8/// Reserve PDAs (supply vault, fee vault, collateral mint/supply) are derived
9/// automatically by the helpers.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct ReserveInfo {
12    /// Reserve account address.
13    pub address: Pubkey,
14    /// The lending market this reserve belongs to.
15    pub lending_market: Pubkey,
16    /// SPL token mint for the reserve's liquidity (e.g. USDC mint).
17    pub liquidity_mint: Pubkey,
18    /// Token program for the liquidity mint (`TOKEN_PROGRAM_ID` or Token-2022).
19    pub liquidity_token_program: Pubkey,
20    /// Pyth oracle, if configured for this reserve.
21    pub pyth_oracle: Option<Pubkey>,
22    /// Switchboard price oracle, if configured.
23    pub switchboard_price_oracle: Option<Pubkey>,
24    /// Switchboard TWAP oracle, if configured.
25    pub switchboard_twap_oracle: Option<Pubkey>,
26    /// Scope prices account, if configured.
27    pub scope_prices: Option<Pubkey>,
28}
29
30/// Obligation metadata needed for building refresh and main instructions.
31///
32/// The caller reads these fields from the deserialized `Obligation` account.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct ObligationInfo {
35    /// Obligation account address.
36    pub address: Pubkey,
37    /// Reserve pubkeys for each **active** deposit position, in order.
38    pub deposit_reserves: Vec<Pubkey>,
39    /// Reserve pubkeys for each **active** borrow position, in order.
40    pub borrow_reserves: Vec<Pubkey>,
41    /// Referrer wallet if the obligation has one (`obligation.referrer`).
42    pub referrer: Option<Pubkey>,
43}
44
45/// Helper: returns `Some(key)` if it is not `Pubkey::default()`.
46pub(super) fn non_default(key: Pubkey) -> Option<Pubkey> {
47    if key == Pubkey::default() {
48        None
49    } else {
50        Some(key)
51    }
52}
53
54impl ReserveInfo {
55    /// Build a `ReserveInfo` directly from raw on-chain account data bytes.
56    ///
57    /// This deserializes the `Reserve` account and extracts the fields needed
58    /// by the helpers. Useful when working with raw RPC responses.
59    pub fn from_account_data(
60        address: Pubkey,
61        data: &[u8],
62    ) -> Result<Self, crate::state::AccountDataError> {
63        let reserve = crate::state::from_account_data::<crate::state::Reserve>(data)?;
64        Ok(Self::from_reserve(address, reserve))
65    }
66
67    /// Build a `ReserveInfo` from a deserialized [`crate::state::Reserve`] account.
68    pub fn from_reserve(address: Pubkey, reserve: &crate::state::Reserve) -> Self {
69        Self {
70            address,
71            lending_market: reserve.lending_market,
72            liquidity_mint: reserve.liquidity.mint_pubkey,
73            liquidity_token_program: reserve.liquidity.token_program,
74            pyth_oracle: non_default(reserve.config.token_info.pyth_configuration.price),
75            switchboard_price_oracle: non_default(
76                reserve
77                    .config
78                    .token_info
79                    .switchboard_configuration
80                    .price_aggregator,
81            ),
82            switchboard_twap_oracle: non_default(
83                reserve
84                    .config
85                    .token_info
86                    .switchboard_configuration
87                    .twap_aggregator,
88            ),
89            scope_prices: non_default(reserve.config.token_info.scope_configuration.price_feed),
90        }
91    }
92}
93
94impl ObligationInfo {
95    /// Build an `ObligationInfo` directly from raw on-chain account data bytes.
96    pub fn from_account_data(
97        address: Pubkey,
98        data: &[u8],
99    ) -> Result<Self, crate::state::AccountDataError> {
100        let obligation = crate::state::from_account_data::<crate::state::Obligation>(data)?;
101        Ok(Self::from_obligation(address, obligation))
102    }
103
104    /// Build an `ObligationInfo` from a deserialized [`crate::state::Obligation`] account.
105    pub fn from_obligation(address: Pubkey, obligation: &crate::state::Obligation) -> Self {
106        let deposit_reserves = obligation
107            .deposits
108            .iter()
109            .filter(|d| d.deposit_reserve != Pubkey::default())
110            .map(|d| d.deposit_reserve)
111            .collect();
112        let borrow_reserves = obligation
113            .borrows
114            .iter()
115            .filter(|b| b.borrow_reserve != Pubkey::default())
116            .map(|b| b.borrow_reserve)
117            .collect();
118        Self {
119            address,
120            deposit_reserves,
121            borrow_reserves,
122            referrer: non_default(obligation.referrer),
123        }
124    }
125}
126
127impl From<(Pubkey, &crate::state::Reserve)> for ReserveInfo {
128    fn from((address, reserve): (Pubkey, &crate::state::Reserve)) -> Self {
129        Self::from_reserve(address, reserve)
130    }
131}
132
133impl TryFrom<(Pubkey, &[u8])> for ReserveInfo {
134    type Error = AccountDataError;
135    fn try_from((address, data): (Pubkey, &[u8])) -> Result<Self, Self::Error> {
136        Self::from_account_data(address, data)
137    }
138}
139
140#[cfg(feature = "solana-account")]
141impl TryFrom<(Pubkey, &solana_account::Account)> for ReserveInfo {
142    type Error = AccountDataError;
143    fn try_from(
144        (address, account): (Pubkey, &solana_account::Account),
145    ) -> Result<Self, Self::Error> {
146        Self::from_account_data(address, &account.data)
147    }
148}
149
150impl From<(Pubkey, &crate::state::Obligation)> for ObligationInfo {
151    fn from((address, obligation): (Pubkey, &crate::state::Obligation)) -> Self {
152        Self::from_obligation(address, obligation)
153    }
154}
155
156impl TryFrom<(Pubkey, &[u8])> for ObligationInfo {
157    type Error = AccountDataError;
158    fn try_from((address, data): (Pubkey, &[u8])) -> Result<Self, Self::Error> {
159        Self::from_account_data(address, data)
160    }
161}
162
163#[cfg(feature = "solana-account")]
164impl TryFrom<(Pubkey, &solana_account::Account)> for ObligationInfo {
165    type Error = AccountDataError;
166    fn try_from(
167        (address, account): (Pubkey, &solana_account::Account),
168    ) -> Result<Self, Self::Error> {
169        Self::from_account_data(address, &account.data)
170    }
171}
172
173/// Reserve info bundled with its farm state pubkeys (crate-internal).
174#[derive(Clone, Debug)]
175pub(crate) struct ReserveWithFarms {
176    pub info: ReserveInfo,
177    pub farm_collateral: Option<Pubkey>,
178    pub farm_debt: Option<Pubkey>,
179}
180
181/// Error returned by [`ObligationContext`] methods.
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum ObligationContextError {
184    /// The specified reserve was not found among the reserves provided at construction time.
185    ReserveNotFound(Pubkey),
186}
187
188impl core::fmt::Display for ObligationContextError {
189    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
190        match self {
191            Self::ReserveNotFound(pk) => write!(f, "reserve not found in context: {pk}"),
192        }
193    }
194}
195
196impl std::error::Error for ObligationContextError {}
197
198/// A high-level context that bundles an obligation with all its reserves and
199/// auto-resolves farm accounts, providing ergonomic one-call methods for every
200/// obligation operation.
201///
202/// # Construction
203///
204/// ```no_run
205/// use klend_interface::helpers::ObligationContext;
206/// # use solana_pubkey::Pubkey;
207/// # let obligation_address = Pubkey::default();
208/// # let obligation = unsafe { std::mem::zeroed::<klend_interface::state::Obligation>() };
209/// # let reserve_addr = Pubkey::default();
210/// # let reserve = unsafe { std::mem::zeroed::<klend_interface::state::Reserve>() };
211///
212/// let ctx = ObligationContext::new(
213///     obligation_address,
214///     &obligation,
215///     &[(reserve_addr, &reserve)],
216/// );
217/// ```
218#[derive(Clone, Debug)]
219pub struct ObligationContext {
220    pub(crate) obligation: ObligationInfo,
221    pub(crate) lending_market: Pubkey,
222    pub(crate) reserves: Vec<ReserveWithFarms>,
223}
224
225impl ObligationContext {
226    /// Build a context from deserialized on-chain accounts.
227    pub fn new(
228        obligation_address: Pubkey,
229        obligation: &crate::state::Obligation,
230        reserves: &[(Pubkey, &crate::state::Reserve)],
231    ) -> Self {
232        let obligation_info = ObligationInfo::from_obligation(obligation_address, obligation);
233        let lending_market = obligation.lending_market;
234        let reserves = reserves
235            .iter()
236            .map(|(addr, reserve)| ReserveWithFarms {
237                info: ReserveInfo::from_reserve(*addr, reserve),
238                farm_collateral: non_default(reserve.farm_collateral),
239                farm_debt: non_default(reserve.farm_debt),
240            })
241            .collect();
242
243        Self {
244            obligation: obligation_info,
245            lending_market,
246            reserves,
247        }
248    }
249
250    /// Parse an obligation and return the unique reserve addresses that must be
251    /// fetched to build a complete context.
252    ///
253    /// Typical RPC flow:
254    /// 1. Fetch the obligation account
255    /// 2. Call `reserve_addresses_for_obligation` to discover which reserves are needed
256    /// 3. Fetch those reserve accounts (e.g. `getMultipleAccounts`)
257    /// 4. Call [`ObligationContext::from_account_data`] with both
258    pub fn reserve_addresses_for_obligation(
259        obligation_data: &[u8],
260    ) -> Result<Vec<Pubkey>, crate::state::AccountDataError> {
261        let obligation =
262            crate::state::from_account_data::<crate::state::Obligation>(obligation_data)?;
263        let mut addrs: Vec<Pubkey> = obligation
264            .deposits
265            .iter()
266            .filter(|d| d.deposit_reserve != Pubkey::default())
267            .map(|d| d.deposit_reserve)
268            .chain(
269                obligation
270                    .borrows
271                    .iter()
272                    .filter(|b| b.borrow_reserve != Pubkey::default())
273                    .map(|b| b.borrow_reserve),
274            )
275            .collect();
276        addrs.sort_unstable();
277        addrs.dedup();
278        Ok(addrs)
279    }
280
281    /// Build a context from raw on-chain account data bytes.
282    ///
283    /// `reserves` must include an entry for every reserve referenced by the
284    /// obligation (see [`ObligationContext::reserve_addresses_for_obligation`]).
285    /// Extra reserves are allowed and will be available for lookups.
286    pub fn from_account_data(
287        obligation_address: Pubkey,
288        obligation_data: &[u8],
289        reserves: &[(Pubkey, &[u8])],
290    ) -> Result<Self, crate::state::AccountDataError> {
291        let obligation =
292            crate::state::from_account_data::<crate::state::Obligation>(obligation_data)?;
293        let parsed_reserves: Result<Vec<_>, _> = reserves
294            .iter()
295            .map(|(addr, data)| {
296                let r = crate::state::from_account_data::<crate::state::Reserve>(data)?;
297                Ok((*addr, r))
298            })
299            .collect();
300        let parsed_reserves = parsed_reserves?;
301        let reserve_pairs: Vec<(Pubkey, &crate::state::Reserve)> =
302            parsed_reserves.iter().map(|(a, r)| (*a, *r)).collect();
303        Ok(Self::new(obligation_address, obligation, &reserve_pairs))
304    }
305
306    /// Build a context from pre-built [`ReserveInfo`] values (no farm auto-resolution).
307    ///
308    /// Use this when you already have `ReserveInfo` and `ObligationInfo` constructed
309    /// from another source, or when farm accounts are not needed.
310    pub fn from_infos(
311        lending_market: Pubkey,
312        obligation: ObligationInfo,
313        reserves: &[ReserveInfo],
314    ) -> Self {
315        Self {
316            obligation,
317            lending_market,
318            reserves: reserves
319                .iter()
320                .map(|info| ReserveWithFarms {
321                    info: info.clone(),
322                    farm_collateral: None,
323                    farm_debt: None,
324                })
325                .collect(),
326        }
327    }
328
329    /// Get the obligation info.
330    pub fn obligation(&self) -> &ObligationInfo {
331        &self.obligation
332    }
333
334    /// Look up a reserve by address.
335    pub fn reserve_info(&self, address: &Pubkey) -> Option<&ReserveInfo> {
336        self.reserves
337            .iter()
338            .find(|r| r.info.address == *address)
339            .map(|r| &r.info)
340    }
341
342    pub(crate) fn find_reserve(&self, address: &Pubkey) -> Option<&ReserveWithFarms> {
343        self.reserves.iter().find(|r| r.info.address == *address)
344    }
345
346    pub(crate) fn all_reserve_infos(&self) -> Vec<ReserveInfo> {
347        self.reserves.iter().map(|r| r.info.clone()).collect()
348    }
349
350    pub(crate) fn collateral_farms(&self, reserve_address: &Pubkey) -> Option<FarmsAccounts> {
351        let r = self.find_reserve(reserve_address)?;
352        let farm_state = r.farm_collateral?;
353        Some(FarmsAccounts {
354            reserve_farm_state: farm_state,
355            obligation_farm_user_state: crate::pda::farms_user_state(
356                &farm_state,
357                &self.obligation.address,
358            )
359            .0,
360        })
361    }
362
363    pub(crate) fn debt_farms(&self, reserve_address: &Pubkey) -> Option<FarmsAccounts> {
364        let r = self.find_reserve(reserve_address)?;
365        let farm_state = r.farm_debt?;
366        Some(FarmsAccounts {
367            reserve_farm_state: farm_state,
368            obligation_farm_user_state: crate::pda::farms_user_state(
369                &farm_state,
370                &self.obligation.address,
371            )
372            .0,
373        })
374    }
375}
376
377/// Farm state accounts for a single reserve+obligation pair.
378#[derive(Clone, Debug, PartialEq, Eq)]
379pub struct FarmsAccounts {
380    pub obligation_farm_user_state: Pubkey,
381    pub reserve_farm_state: Pubkey,
382}
383
384/// Optional progress-callback accounts for `enqueue_to_withdraw` and
385/// `withdraw_queued_liquidity`.
386#[derive(Clone, Debug, PartialEq, Eq)]
387pub struct CallbackAccounts {
388    pub progress_callback_type: crate::types::ProgressCallbackType,
389    pub custom_account_0: Option<Pubkey>,
390    pub custom_account_1: Option<Pubkey>,
391}
392
393impl CallbackAccounts {
394    /// Extract callback configuration from a deserialized [`crate::state::WithdrawTicket`].
395    ///
396    /// Returns `None` if the ticket has no callback (`ProgressCallbackType::None`).
397    pub fn from_withdraw_ticket(ticket: &crate::state::WithdrawTicket) -> Option<Self> {
398        let cb_type = match ticket.progress_callback_type {
399            0 => return None,
400            1 => crate::types::ProgressCallbackType::KlendQueueAccountingHandlerOnKvault,
401            _ => return None,
402        };
403        Some(Self {
404            progress_callback_type: cb_type,
405            custom_account_0: non_default(ticket.progress_callback_custom_accounts[0]),
406            custom_account_1: non_default(ticket.progress_callback_custom_accounts[1]),
407        })
408    }
409
410    /// Extract callback configuration from raw withdraw ticket account data bytes.
411    ///
412    /// Returns `Ok(None)` if the ticket has no callback.
413    pub fn from_withdraw_ticket_data(
414        data: &[u8],
415    ) -> Result<Option<Self>, crate::state::AccountDataError> {
416        let ticket = crate::state::from_account_data::<crate::state::WithdrawTicket>(data)?;
417        Ok(Self::from_withdraw_ticket(ticket))
418    }
419
420    /// Extract callback configuration from a [`solana_account::Account`].
421    ///
422    /// Returns `Ok(None)` if the ticket has no callback.
423    #[cfg(feature = "solana-account")]
424    pub fn from_withdraw_ticket_account(
425        account: &solana_account::Account,
426    ) -> Result<Option<Self>, crate::state::AccountDataError> {
427        Self::from_withdraw_ticket_data(&account.data)
428    }
429}