Skip to main content

kvault_interface/helpers/
info.rs

1use std::collections::HashMap;
2
3use solana_pubkey::Pubkey;
4
5use crate::state::{self, VaultState};
6
7/// One active vault allocation: a Klend reserve and the lending market it
8/// belongs to.
9///
10/// The lending market is not stored on the [`VaultState`]; it lives on each
11/// reserve account and must be fetched alongside it (see
12/// [`VaultInfo::from_vault_state`]).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct VaultReserve {
15    /// Klend reserve address with an active allocation.
16    pub reserve: Pubkey,
17    /// The lending market this reserve belongs to.
18    pub lending_market: Pubkey,
19}
20
21/// On-chain vault data that cannot be derived from PDAs.
22///
23/// The caller reads these fields from the deserialized [`VaultState`] account.
24/// Vault PDAs (authority, token vault, shares mint) and remaining accounts
25/// are derived automatically by the helpers.
26///
27/// # Reserve slot-ordering contract
28///
29/// [`reserves`](Self::reserves) mirrors the on-chain non-empty
30/// `vault_allocation_strategy[]` slots **in slot order**. The on-chain refresh
31/// check (`check_allocation_reserves_refreshed`) walks the refresh remaining
32/// accounts in exactly that order, so the order must be preserved. The
33/// constructors enforce this by re-deriving the order from the allocation
34/// strategy regardless of the order in which reserve data is supplied, so the
35/// refresh check cannot be violated through this type.
36pub struct VaultInfo {
37    /// Vault account address.
38    pub address: Pubkey,
39    /// SPL token mint for the vault's underlying asset (e.g. USDC).
40    pub token_mint: Pubkey,
41    /// Token program for [`token_mint`](Self::token_mint) (`TOKEN_PROGRAM_ID` or Token-2022).
42    pub token_program: Pubkey,
43    /// Active allocations (reserve + lending market) in `vault_allocation_strategy[]` slot order.
44    reserves: Vec<VaultReserve>,
45}
46
47/// Errors returned when building a [`VaultInfo`].
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum VaultInfoError {
50    /// Failed to deserialize the vault account data.
51    AccountData(state::AccountDataError),
52    /// An active allocation reserve had no matching [`ReserveInfo`] provided.
53    MissingReserve(Pubkey),
54}
55
56impl core::fmt::Display for VaultInfoError {
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        match self {
59            Self::AccountData(err) => write!(f, "{err}"),
60            Self::MissingReserve(reserve) => {
61                write!(f, "no ReserveInfo provided for active reserve {reserve}")
62            }
63        }
64    }
65}
66
67impl std::error::Error for VaultInfoError {}
68
69impl From<state::AccountDataError> for VaultInfoError {
70    fn from(err: state::AccountDataError) -> Self {
71        Self::AccountData(err)
72    }
73}
74
75impl VaultInfo {
76    /// Addresses of the reserves with active allocations, in
77    /// `vault_allocation_strategy[]` slot order.
78    ///
79    /// Use this to learn which reserve accounts to fetch *before* building a
80    /// [`VaultInfo`] (the lending markets needed by the constructors live on
81    /// those reserve accounts).
82    pub fn active_reserve_addresses(vault: &VaultState) -> Vec<Pubkey> {
83        vault
84            .vault_allocation_strategy
85            .iter()
86            .map(|a| a.reserve)
87            .filter(|r| r != &Pubkey::default())
88            .collect()
89    }
90
91    /// Build from a deserialized [`VaultState`] and the fetched reserve data.
92    ///
93    /// The reserve+lending-market list is built by iterating the active
94    /// allocations **in slot order** and looking up the matching [`ReserveInfo`]
95    /// by address, so the order of `reserves` does not matter. Returns
96    /// [`VaultInfoError::MissingReserve`] if any active allocation reserve has
97    /// no matching [`ReserveInfo`].
98    pub fn from_vault_state(
99        address: Pubkey,
100        vault: &VaultState,
101        reserves: &[ReserveInfo],
102    ) -> Result<Self, VaultInfoError> {
103        let lending_markets: HashMap<Pubkey, Pubkey> = reserves
104            .iter()
105            .map(|r| (r.address, r.lending_market))
106            .collect();
107
108        let reserves = Self::active_reserve_addresses(vault)
109            .into_iter()
110            .map(|reserve| {
111                let lending_market = lending_markets
112                    .get(&reserve)
113                    .copied()
114                    .ok_or(VaultInfoError::MissingReserve(reserve))?;
115                Ok(VaultReserve {
116                    reserve,
117                    lending_market,
118                })
119            })
120            .collect::<Result<Vec<_>, VaultInfoError>>()?;
121
122        Ok(Self {
123            address,
124            token_mint: vault.token_mint,
125            token_program: vault.token_program,
126            reserves,
127        })
128    }
129
130    /// Build from raw account bytes (includes 8-byte discriminator) and the
131    /// fetched reserve data.
132    ///
133    /// See [`from_vault_state`](Self::from_vault_state) for the slot-ordering
134    /// contract and the `reserves` lookup semantics.
135    pub fn from_account_data(
136        address: Pubkey,
137        data: &[u8],
138        reserves: &[ReserveInfo],
139    ) -> Result<Self, VaultInfoError> {
140        let vault = state::from_account_data::<VaultState>(data)?;
141        Self::from_vault_state(address, vault, reserves)
142    }
143
144    /// Active allocations (reserve + lending market) in
145    /// `vault_allocation_strategy[]` slot order.
146    ///
147    /// The order mirrors the on-chain non-empty allocation slots and is
148    /// enforced by the constructors (see the [type-level
149    /// docs](VaultInfo#reserve-slot-ordering-contract)), so it is safe to use
150    /// directly to build the refresh remaining accounts.
151    pub fn reserves(&self) -> &[VaultReserve] {
152        &self.reserves
153    }
154
155    /// Addresses of the reserves with active allocations, in slot order.
156    ///
157    /// Convenience iterator for read-only consumers that only need the reserve
158    /// pubkeys.
159    pub fn reserve_addresses(&self) -> impl Iterator<Item = Pubkey> + '_ {
160        self.reserves.iter().map(|r| r.reserve)
161    }
162}
163
164/// On-chain Klend reserve data needed by helpers that interact with reserves
165/// (withdraw, invest, redeem).
166///
167/// The caller reads these fields from the deserialized
168/// [`Reserve`](klend_interface::state::Reserve) account. Reserve PDAs are
169/// used directly (not re-derived) because they are stored on the reserve itself.
170pub struct ReserveInfo {
171    /// Reserve account address.
172    pub address: Pubkey,
173    /// The lending market this reserve belongs to.
174    pub lending_market: Pubkey,
175    /// Token account holding the reserve's available liquidity.
176    pub liquidity_supply_vault: Pubkey,
177    /// SPL mint for the reserve's collateral tokens (cTokens).
178    pub collateral_mint: Pubkey,
179}
180
181impl ReserveInfo {
182    /// Build from a deserialized klend [`Reserve`](klend_interface::state::Reserve).
183    pub fn from_reserve(address: Pubkey, reserve: &klend_interface::state::Reserve) -> Self {
184        Self {
185            address,
186            lending_market: reserve.lending_market,
187            liquidity_supply_vault: reserve.liquidity.supply_vault,
188            collateral_mint: reserve.collateral.mint_pubkey,
189        }
190    }
191
192    /// Build from raw klend reserve account bytes (includes 8-byte discriminator).
193    pub fn from_account_data(
194        address: Pubkey,
195        data: &[u8],
196    ) -> Result<Self, klend_interface::state::AccountDataError> {
197        let reserve =
198            klend_interface::state::from_account_data::<klend_interface::state::Reserve>(data)?;
199        Ok(Self::from_reserve(address, reserve))
200    }
201}
202
203impl From<(Pubkey, &klend_interface::state::Reserve)> for ReserveInfo {
204    fn from((address, reserve): (Pubkey, &klend_interface::state::Reserve)) -> Self {
205        Self::from_reserve(address, reserve)
206    }
207}
208
209impl TryFrom<(Pubkey, &[u8])> for ReserveInfo {
210    type Error = klend_interface::state::AccountDataError;
211    fn try_from((address, data): (Pubkey, &[u8])) -> Result<Self, Self::Error> {
212        Self::from_account_data(address, data)
213    }
214}