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}