klend-interface 0.6.0

Instruction builders for Kamino Lending (klend) — no anchor-lang dependency
Documentation
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
use solana_pubkey::Pubkey;

use crate::state::AccountDataError;

/// On-chain reserve data that cannot be derived from PDAs.
///
/// The caller reads these fields from the deserialized `Reserve` account.
/// Reserve PDAs (supply vault, fee vault, collateral mint/supply) are derived
/// automatically by the helpers.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReserveInfo {
    /// Reserve account address.
    pub address: Pubkey,
    /// The lending market this reserve belongs to.
    pub lending_market: Pubkey,
    /// SPL token mint for the reserve's liquidity (e.g. USDC mint).
    pub liquidity_mint: Pubkey,
    /// Token program for the liquidity mint (`TOKEN_PROGRAM_ID` or Token-2022).
    pub liquidity_token_program: Pubkey,
    /// Pyth oracle, if configured for this reserve.
    pub pyth_oracle: Option<Pubkey>,
    /// Switchboard price oracle, if configured.
    pub switchboard_price_oracle: Option<Pubkey>,
    /// Switchboard TWAP oracle, if configured.
    pub switchboard_twap_oracle: Option<Pubkey>,
    /// Scope prices account, if configured.
    pub scope_prices: Option<Pubkey>,
}

/// Obligation metadata needed for building refresh and main instructions.
///
/// The caller reads these fields from the deserialized `Obligation` account.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObligationInfo {
    /// Obligation account address.
    pub address: Pubkey,
    /// Reserve pubkeys for each **active** deposit position, in order.
    pub deposit_reserves: Vec<Pubkey>,
    /// Reserve pubkeys for each **active** borrow position, in order.
    pub borrow_reserves: Vec<Pubkey>,
    /// Referrer wallet if the obligation has one (`obligation.referrer`).
    pub referrer: Option<Pubkey>,
}

/// Helper: returns `Some(key)` if it is not `Pubkey::default()`.
pub(super) fn non_default(key: Pubkey) -> Option<Pubkey> {
    if key == Pubkey::default() {
        None
    } else {
        Some(key)
    }
}

impl ReserveInfo {
    /// Build a `ReserveInfo` directly from raw on-chain account data bytes.
    ///
    /// This deserializes the `Reserve` account and extracts the fields needed
    /// by the helpers. Useful when working with raw RPC responses.
    pub fn from_account_data(
        address: Pubkey,
        data: &[u8],
    ) -> Result<Self, crate::state::AccountDataError> {
        let reserve = crate::state::from_account_data::<crate::state::Reserve>(data)?;
        Ok(Self::from_reserve(address, reserve))
    }

    /// Build a `ReserveInfo` from a deserialized [`crate::state::Reserve`] account.
    pub fn from_reserve(address: Pubkey, reserve: &crate::state::Reserve) -> Self {
        Self {
            address,
            lending_market: reserve.lending_market,
            liquidity_mint: reserve.liquidity.mint_pubkey,
            liquidity_token_program: reserve.liquidity.token_program,
            pyth_oracle: non_default(reserve.config.token_info.pyth_configuration.price),
            switchboard_price_oracle: non_default(
                reserve
                    .config
                    .token_info
                    .switchboard_configuration
                    .price_aggregator,
            ),
            switchboard_twap_oracle: non_default(
                reserve
                    .config
                    .token_info
                    .switchboard_configuration
                    .twap_aggregator,
            ),
            scope_prices: non_default(reserve.config.token_info.scope_configuration.price_feed),
        }
    }
}

impl ObligationInfo {
    /// Build an `ObligationInfo` directly from raw on-chain account data bytes.
    pub fn from_account_data(
        address: Pubkey,
        data: &[u8],
    ) -> Result<Self, crate::state::AccountDataError> {
        let obligation = crate::state::from_account_data::<crate::state::Obligation>(data)?;
        Ok(Self::from_obligation(address, obligation))
    }

    /// Build an `ObligationInfo` from a deserialized [`crate::state::Obligation`] account.
    pub fn from_obligation(address: Pubkey, obligation: &crate::state::Obligation) -> Self {
        let deposit_reserves = obligation
            .deposits
            .iter()
            .filter(|d| d.deposit_reserve != Pubkey::default())
            .map(|d| d.deposit_reserve)
            .collect();
        let borrow_reserves = obligation
            .borrows
            .iter()
            .filter(|b| b.borrow_reserve != Pubkey::default())
            .map(|b| b.borrow_reserve)
            .collect();
        Self {
            address,
            deposit_reserves,
            borrow_reserves,
            referrer: non_default(obligation.referrer),
        }
    }
}

impl From<(Pubkey, &crate::state::Reserve)> for ReserveInfo {
    fn from((address, reserve): (Pubkey, &crate::state::Reserve)) -> Self {
        Self::from_reserve(address, reserve)
    }
}

impl TryFrom<(Pubkey, &[u8])> for ReserveInfo {
    type Error = AccountDataError;
    fn try_from((address, data): (Pubkey, &[u8])) -> Result<Self, Self::Error> {
        Self::from_account_data(address, data)
    }
}

#[cfg(feature = "solana-account")]
impl TryFrom<(Pubkey, &solana_account::Account)> for ReserveInfo {
    type Error = AccountDataError;
    fn try_from(
        (address, account): (Pubkey, &solana_account::Account),
    ) -> Result<Self, Self::Error> {
        Self::from_account_data(address, &account.data)
    }
}

impl From<(Pubkey, &crate::state::Obligation)> for ObligationInfo {
    fn from((address, obligation): (Pubkey, &crate::state::Obligation)) -> Self {
        Self::from_obligation(address, obligation)
    }
}

impl TryFrom<(Pubkey, &[u8])> for ObligationInfo {
    type Error = AccountDataError;
    fn try_from((address, data): (Pubkey, &[u8])) -> Result<Self, Self::Error> {
        Self::from_account_data(address, data)
    }
}

#[cfg(feature = "solana-account")]
impl TryFrom<(Pubkey, &solana_account::Account)> for ObligationInfo {
    type Error = AccountDataError;
    fn try_from(
        (address, account): (Pubkey, &solana_account::Account),
    ) -> Result<Self, Self::Error> {
        Self::from_account_data(address, &account.data)
    }
}

/// Reserve info bundled with its farm state pubkeys (crate-internal).
#[derive(Clone, Debug)]
pub(crate) struct ReserveWithFarms {
    pub info: ReserveInfo,
    pub farm_collateral: Option<Pubkey>,
    pub farm_debt: Option<Pubkey>,
}

/// Error returned by [`ObligationContext`] methods.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ObligationContextError {
    /// The specified reserve was not found among the reserves provided at construction time.
    ReserveNotFound(Pubkey),
}

impl core::fmt::Display for ObligationContextError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::ReserveNotFound(pk) => write!(f, "reserve not found in context: {pk}"),
        }
    }
}

impl std::error::Error for ObligationContextError {}

/// A high-level context that bundles an obligation with all its reserves and
/// auto-resolves farm accounts, providing ergonomic one-call methods for every
/// obligation operation.
///
/// # Construction
///
/// ```no_run
/// use klend_interface::helpers::ObligationContext;
/// # use solana_pubkey::Pubkey;
/// # let obligation_address = Pubkey::default();
/// # let obligation = unsafe { std::mem::zeroed::<klend_interface::state::Obligation>() };
/// # let reserve_addr = Pubkey::default();
/// # let reserve = unsafe { std::mem::zeroed::<klend_interface::state::Reserve>() };
///
/// let ctx = ObligationContext::new(
///     obligation_address,
///     &obligation,
///     &[(reserve_addr, &reserve)],
/// );
/// ```
#[derive(Clone, Debug)]
pub struct ObligationContext {
    pub(crate) obligation: ObligationInfo,
    pub(crate) lending_market: Pubkey,
    pub(crate) reserves: Vec<ReserveWithFarms>,
}

impl ObligationContext {
    /// Build a context from deserialized on-chain accounts.
    pub fn new(
        obligation_address: Pubkey,
        obligation: &crate::state::Obligation,
        reserves: &[(Pubkey, &crate::state::Reserve)],
    ) -> Self {
        let obligation_info = ObligationInfo::from_obligation(obligation_address, obligation);
        let lending_market = obligation.lending_market;
        let reserves = reserves
            .iter()
            .map(|(addr, reserve)| ReserveWithFarms {
                info: ReserveInfo::from_reserve(*addr, reserve),
                farm_collateral: non_default(reserve.farm_collateral),
                farm_debt: non_default(reserve.farm_debt),
            })
            .collect();

        Self {
            obligation: obligation_info,
            lending_market,
            reserves,
        }
    }

    /// Parse an obligation and return the unique reserve addresses that must be
    /// fetched to build a complete context.
    ///
    /// Typical RPC flow:
    /// 1. Fetch the obligation account
    /// 2. Call `reserve_addresses_for_obligation` to discover which reserves are needed
    /// 3. Fetch those reserve accounts (e.g. `getMultipleAccounts`)
    /// 4. Call [`ObligationContext::from_account_data`] with both
    pub fn reserve_addresses_for_obligation(
        obligation_data: &[u8],
    ) -> Result<Vec<Pubkey>, crate::state::AccountDataError> {
        let obligation =
            crate::state::from_account_data::<crate::state::Obligation>(obligation_data)?;
        let mut addrs: Vec<Pubkey> = obligation
            .deposits
            .iter()
            .filter(|d| d.deposit_reserve != Pubkey::default())
            .map(|d| d.deposit_reserve)
            .chain(
                obligation
                    .borrows
                    .iter()
                    .filter(|b| b.borrow_reserve != Pubkey::default())
                    .map(|b| b.borrow_reserve),
            )
            .collect();
        addrs.sort_unstable();
        addrs.dedup();
        Ok(addrs)
    }

    /// Build a context from raw on-chain account data bytes.
    ///
    /// `reserves` must include an entry for every reserve referenced by the
    /// obligation (see [`ObligationContext::reserve_addresses_for_obligation`]).
    /// Extra reserves are allowed and will be available for lookups.
    pub fn from_account_data(
        obligation_address: Pubkey,
        obligation_data: &[u8],
        reserves: &[(Pubkey, &[u8])],
    ) -> Result<Self, crate::state::AccountDataError> {
        let obligation =
            crate::state::from_account_data::<crate::state::Obligation>(obligation_data)?;
        let parsed_reserves: Result<Vec<_>, _> = reserves
            .iter()
            .map(|(addr, data)| {
                let r = crate::state::from_account_data::<crate::state::Reserve>(data)?;
                Ok((*addr, r))
            })
            .collect();
        let parsed_reserves = parsed_reserves?;
        let reserve_pairs: Vec<(Pubkey, &crate::state::Reserve)> =
            parsed_reserves.iter().map(|(a, r)| (*a, *r)).collect();
        Ok(Self::new(obligation_address, obligation, &reserve_pairs))
    }

    /// Build a context from pre-built [`ReserveInfo`] values (no farm auto-resolution).
    ///
    /// Use this when you already have `ReserveInfo` and `ObligationInfo` constructed
    /// from another source, or when farm accounts are not needed.
    pub fn from_infos(
        lending_market: Pubkey,
        obligation: ObligationInfo,
        reserves: &[ReserveInfo],
    ) -> Self {
        Self {
            obligation,
            lending_market,
            reserves: reserves
                .iter()
                .map(|info| ReserveWithFarms {
                    info: info.clone(),
                    farm_collateral: None,
                    farm_debt: None,
                })
                .collect(),
        }
    }

    /// Get the obligation info.
    pub fn obligation(&self) -> &ObligationInfo {
        &self.obligation
    }

    /// Look up a reserve by address.
    pub fn reserve_info(&self, address: &Pubkey) -> Option<&ReserveInfo> {
        self.reserves
            .iter()
            .find(|r| r.info.address == *address)
            .map(|r| &r.info)
    }

    pub(crate) fn find_reserve(&self, address: &Pubkey) -> Option<&ReserveWithFarms> {
        self.reserves.iter().find(|r| r.info.address == *address)
    }

    pub(crate) fn all_reserve_infos(&self) -> Vec<ReserveInfo> {
        self.reserves.iter().map(|r| r.info.clone()).collect()
    }

    pub(crate) fn collateral_farms(&self, reserve_address: &Pubkey) -> Option<FarmsAccounts> {
        let r = self.find_reserve(reserve_address)?;
        let farm_state = r.farm_collateral?;
        Some(FarmsAccounts {
            reserve_farm_state: farm_state,
            obligation_farm_user_state: crate::pda::farms_user_state(
                &farm_state,
                &self.obligation.address,
            )
            .0,
        })
    }

    pub(crate) fn debt_farms(&self, reserve_address: &Pubkey) -> Option<FarmsAccounts> {
        let r = self.find_reserve(reserve_address)?;
        let farm_state = r.farm_debt?;
        Some(FarmsAccounts {
            reserve_farm_state: farm_state,
            obligation_farm_user_state: crate::pda::farms_user_state(
                &farm_state,
                &self.obligation.address,
            )
            .0,
        })
    }
}

/// Farm state accounts for a single reserve+obligation pair.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FarmsAccounts {
    pub obligation_farm_user_state: Pubkey,
    pub reserve_farm_state: Pubkey,
}

/// Optional progress-callback accounts for `enqueue_to_withdraw` and
/// `withdraw_queued_liquidity`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CallbackAccounts {
    pub progress_callback_type: crate::types::ProgressCallbackType,
    pub custom_account_0: Option<Pubkey>,
    pub custom_account_1: Option<Pubkey>,
}

impl CallbackAccounts {
    /// Extract callback configuration from a deserialized [`crate::state::WithdrawTicket`].
    ///
    /// Returns `None` if the ticket has no callback (`ProgressCallbackType::None`).
    pub fn from_withdraw_ticket(ticket: &crate::state::WithdrawTicket) -> Option<Self> {
        let cb_type = match ticket.progress_callback_type {
            0 => return None,
            1 => crate::types::ProgressCallbackType::KlendQueueAccountingHandlerOnKvault,
            _ => return None,
        };
        Some(Self {
            progress_callback_type: cb_type,
            custom_account_0: non_default(ticket.progress_callback_custom_accounts[0]),
            custom_account_1: non_default(ticket.progress_callback_custom_accounts[1]),
        })
    }

    /// Extract callback configuration from raw withdraw ticket account data bytes.
    ///
    /// Returns `Ok(None)` if the ticket has no callback.
    pub fn from_withdraw_ticket_data(
        data: &[u8],
    ) -> Result<Option<Self>, crate::state::AccountDataError> {
        let ticket = crate::state::from_account_data::<crate::state::WithdrawTicket>(data)?;
        Ok(Self::from_withdraw_ticket(ticket))
    }

    /// Extract callback configuration from a [`solana_account::Account`].
    ///
    /// Returns `Ok(None)` if the ticket has no callback.
    #[cfg(feature = "solana-account")]
    pub fn from_withdraw_ticket_account(
        account: &solana_account::Account,
    ) -> Result<Option<Self>, crate::state::AccountDataError> {
        Self::from_withdraw_ticket_data(&account.data)
    }
}