@kamino-finance/klend-sdk 7.0.2 → 7.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/classes/manager.d.ts +12 -9
  2. package/dist/classes/manager.d.ts.map +1 -1
  3. package/dist/classes/manager.js +13 -47
  4. package/dist/classes/manager.js.map +1 -1
  5. package/dist/classes/market.d.ts +9 -7
  6. package/dist/classes/market.d.ts.map +1 -1
  7. package/dist/classes/market.js +49 -16
  8. package/dist/classes/market.js.map +1 -1
  9. package/dist/classes/reserve.d.ts +2 -2
  10. package/dist/classes/reserve.d.ts.map +1 -1
  11. package/dist/classes/reserve.js +2 -2
  12. package/dist/classes/reserve.js.map +1 -1
  13. package/dist/classes/vault.d.ts +20 -9
  14. package/dist/classes/vault.d.ts.map +1 -1
  15. package/dist/classes/vault.js +72 -24
  16. package/dist/classes/vault.js.map +1 -1
  17. package/dist/client/commands/initFarmsForReserve.d.ts.map +1 -1
  18. package/dist/client/commands/initFarmsForReserve.js +1 -2
  19. package/dist/client/commands/initFarmsForReserve.js.map +1 -1
  20. package/dist/manager/client_kamino_manager.js +4 -0
  21. package/dist/manager/client_kamino_manager.js.map +1 -1
  22. package/dist/utils/farmUtils.d.ts +10 -0
  23. package/dist/utils/farmUtils.d.ts.map +1 -0
  24. package/dist/utils/farmUtils.js +41 -0
  25. package/dist/utils/farmUtils.js.map +1 -0
  26. package/dist/utils/oracle.d.ts +1 -1
  27. package/dist/utils/oracle.d.ts.map +1 -1
  28. package/dist/utils/oracle.js +2 -2
  29. package/dist/utils/oracle.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/classes/manager.ts +30 -64
  32. package/src/classes/market.ts +91 -21
  33. package/src/classes/reserve.ts +4 -2
  34. package/src/classes/vault.ts +126 -26
  35. package/src/client/commands/initFarmsForReserve.ts +1 -1
  36. package/src/manager/client_kamino_manager.ts +4 -0
  37. package/src/utils/farmUtils.ts +73 -0
  38. package/src/utils/oracle.ts +3 -2
@@ -20,6 +20,7 @@ import { KaminoObligation } from './obligation';
20
20
  import { KaminoReserve, KaminoReserveRpcApi } from './reserve';
21
21
  import { LendingMarket, Obligation, ReferrerTokenState, Reserve, UserMetadata } from '../@codegen/klend/accounts';
22
22
  import {
23
+ AllOracleAccounts,
23
24
  cacheOrGetPythPrices,
24
25
  cacheOrGetScopePrice,
25
26
  cacheOrGetSwitchboardPrice,
@@ -48,7 +49,7 @@ import { PROGRAM_ID } from '../@codegen/klend/programId';
48
49
  import { Scope, U16_MAX } from '@kamino-finance/scope-sdk';
49
50
  import { OraclePrices } from '@kamino-finance/scope-sdk/dist/@codegen/scope/accounts/OraclePrices';
50
51
  import { Fraction } from './fraction';
51
- import { chunks, KaminoPrices, MintToPriceMap } from '@kamino-finance/kliquidity-sdk';
52
+ import { batchFetch, chunks, KaminoPrices, MintToPriceMap } from '@kamino-finance/kliquidity-sdk';
52
53
  import { parseTokenSymbol, parseZeroPaddedUtf8 } from './utils';
53
54
  import { ObligationZP } from '../@codegen/klend/zero_padding';
54
55
  import { checkDefined } from '../utils/validations';
@@ -99,6 +100,10 @@ export class KaminoMarket {
99
100
  recentSlotDurationMs: number,
100
101
  programId: Address = PROGRAM_ID
101
102
  ) {
103
+ if (recentSlotDurationMs <= 0) {
104
+ throw new Error('Recent slot duration cannot be 0');
105
+ }
106
+
102
107
  this.address = marketAddress;
103
108
  this.rpc = rpc;
104
109
  this.state = state;
@@ -134,10 +139,6 @@ export class KaminoMarket {
134
139
  return null;
135
140
  }
136
141
 
137
- if (recentSlotDurationMs <= 0) {
138
- throw new Error('Recent slot duration cannot be 0');
139
- }
140
-
141
142
  const reserves = withReserves
142
143
  ? await getReservesForMarket(marketAddress, rpc, programId, recentSlotDurationMs)
143
144
  : new Map<Address, KaminoReserve>();
@@ -156,6 +157,68 @@ export class KaminoMarket {
156
157
  return new KaminoMarket(connection, market, marketAddress, reserves, recentSlotDurationMs, programId);
157
158
  }
158
159
 
160
+ static async loadMultiple(
161
+ connection: Rpc<KaminoMarketRpcApi>,
162
+ markets: Address[],
163
+ recentSlotDurationMs: number,
164
+ programId: Address = PROGRAM_ID,
165
+ withReserves: boolean = true,
166
+ oracleAccounts?: AllOracleAccounts
167
+ ) {
168
+ const marketStates = await batchFetch(markets, (market) =>
169
+ LendingMarket.fetchMultiple(connection, market, programId)
170
+ );
171
+ const kaminoMarkets = new Map<Address, KaminoMarket>();
172
+ for (let i = 0; i < markets.length; i++) {
173
+ const market = marketStates[i];
174
+ const marketAddress = markets[i];
175
+ if (market === null) {
176
+ throw Error(`Could not fetch LendingMarket account state for market ${marketAddress}`);
177
+ }
178
+
179
+ const marketReserves = withReserves
180
+ ? await getReservesForMarket(marketAddress, connection, programId, recentSlotDurationMs, oracleAccounts)
181
+ : new Map<Address, KaminoReserve>();
182
+
183
+ kaminoMarkets.set(
184
+ marketAddress,
185
+ new KaminoMarket(connection, market, marketAddress, marketReserves, recentSlotDurationMs, programId)
186
+ );
187
+ }
188
+ return kaminoMarkets;
189
+ }
190
+
191
+ static async loadMultipleWithReserves(
192
+ connection: Rpc<KaminoMarketRpcApi>,
193
+ markets: Address[],
194
+ reserves: Map<Address, Map<Address, KaminoReserve>>,
195
+ recentSlotDurationMs: number,
196
+ programId: Address = PROGRAM_ID
197
+ ) {
198
+ const marketStates = await batchFetch(markets, (market) =>
199
+ LendingMarket.fetchMultiple(connection, market, programId)
200
+ );
201
+ const kaminoMarkets = new Map<Address, KaminoMarket>();
202
+ for (let i = 0; i < markets.length; i++) {
203
+ const market = marketStates[i];
204
+ const marketAddress = markets[i];
205
+ if (market === null) {
206
+ throw Error(`Could not fetch LendingMarket account state for market ${marketAddress}`);
207
+ }
208
+ const marketReserves = reserves.get(marketAddress);
209
+ if (!marketReserves) {
210
+ throw Error(
211
+ `Could not get reserves for market ${marketAddress} from the reserves map argument supplied to this method`
212
+ );
213
+ }
214
+ kaminoMarkets.set(
215
+ marketAddress,
216
+ new KaminoMarket(connection, market, marketAddress, marketReserves, recentSlotDurationMs, programId)
217
+ );
218
+ }
219
+ return kaminoMarkets;
220
+ }
221
+
159
222
  async reload(): Promise<void> {
160
223
  const market = await LendingMarket.fetch(this.rpc, this.getAddress(), this.programId);
161
224
  if (market === null) {
@@ -395,7 +458,7 @@ export class KaminoMarket {
395
458
  : debtReserve.getMaxBorrowAmountWithCollReserve(this, collReserve, slot);
396
459
  }
397
460
 
398
- async loadReserves() {
461
+ async loadReserves(oracleAccounts?: AllOracleAccounts) {
399
462
  const addresses = [...this.reserves.keys()];
400
463
  const reserveAccounts = await this.rpc
401
464
  .getMultipleAccounts(addresses, { commitment: 'processed', encoding: 'base64' })
@@ -411,7 +474,7 @@ export class KaminoMarket {
411
474
  }
412
475
  return reserveAccount;
413
476
  });
414
- const reservesAndOracles = await getTokenOracleData(this.getRpc(), deserializedReserves);
477
+ const reservesAndOracles = await getTokenOracleData(this.getRpc(), deserializedReserves, oracleAccounts);
415
478
  const kaminoReserves = new Map<Address, KaminoReserve>();
416
479
  reservesAndOracles.forEach(([reserve, oracle], index) => {
417
480
  if (!oracle) {
@@ -881,9 +944,13 @@ export class KaminoMarket {
881
944
  return finalObligations;
882
945
  }
883
946
 
884
- async getAllUserObligations(user: Address, commitment: Commitment = 'processed'): Promise<KaminoObligation[]> {
947
+ async getAllUserObligations(
948
+ user: Address,
949
+ commitment: Commitment = 'processed',
950
+ slot?: bigint
951
+ ): Promise<KaminoObligation[]> {
885
952
  const [currentSlot, obligations] = await Promise.all([
886
- this.rpc.getSlot().send(),
953
+ slot !== undefined ? Promise.resolve(slot) : this.rpc.getSlot().send(),
887
954
  this.rpc
888
955
  .getProgramAccounts(this.programId, {
889
956
  filters: [
@@ -1261,8 +1328,7 @@ export class KaminoMarket {
1261
1328
  /**
1262
1329
  * Get all Scope prices used by all the market reserves
1263
1330
  */
1264
- async getAllScopePrices(scope: Scope): Promise<KaminoPrices> {
1265
- const allOraclePrices = await this.getReserveOraclePrices(scope);
1331
+ async getAllScopePrices(scope: Scope, allOraclePrices: Map<Address, OraclePrices>): Promise<KaminoPrices> {
1266
1332
  const spot: MintToPriceMap = {};
1267
1333
  const twaps: MintToPriceMap = {};
1268
1334
  for (const reserve of this.reserves.values()) {
@@ -1271,7 +1337,7 @@ export class KaminoMarket {
1271
1337
  const oracle = reserve.state.config.tokenInfo.scopeConfiguration.priceFeed;
1272
1338
  const chain = reserve.state.config.tokenInfo.scopeConfiguration.priceChain;
1273
1339
  const twapChain = reserve.state.config.tokenInfo.scopeConfiguration.twapChain.filter((x) => x > 0);
1274
- const oraclePrices = allOraclePrices.get(reserve.address);
1340
+ const oraclePrices = allOraclePrices.get(oracle);
1275
1341
  if (oraclePrices && oracle && isNotNullPubkey(oracle) && chain && Scope.isScopeChainValid(chain)) {
1276
1342
  const spotPrice = await scope.getPriceFromChain(chain, oraclePrices);
1277
1343
  spot[tokenMint] = { price: spotPrice.price, name: tokenName };
@@ -1287,16 +1353,18 @@ export class KaminoMarket {
1287
1353
  /**
1288
1354
  * Get all Scope/Pyth/Switchboard prices used by all the market reserves
1289
1355
  */
1290
- async getAllPrices(): Promise<KlendPrices> {
1356
+ async getAllPrices(oracleAccounts?: AllOracleAccounts): Promise<KlendPrices> {
1291
1357
  const klendPrices: KlendPrices = {
1292
1358
  scope: { spot: {}, twap: {} },
1293
1359
  pyth: { spot: {}, twap: {} },
1294
1360
  switchboard: { spot: {}, twap: {} },
1295
1361
  };
1296
- const allOracleAccounts = await getAllOracleAccounts(
1297
- this.rpc,
1298
- this.getReserves().map((x) => x.state)
1299
- );
1362
+ const allOracleAccounts =
1363
+ oracleAccounts ??
1364
+ (await getAllOracleAccounts(
1365
+ this.rpc,
1366
+ this.getReserves().map((x) => x.state)
1367
+ ));
1300
1368
  const pythCache = new Map<Address, PythPrices>();
1301
1369
  const switchboardCache = new Map<Address, CandidatePrice>();
1302
1370
  const scopeCache = new Map<Address, OraclePrices>();
@@ -1506,7 +1574,8 @@ export async function getReservesForMarket(
1506
1574
  marketAddress: Address,
1507
1575
  rpc: Rpc<KaminoReserveRpcApi>,
1508
1576
  programId: Address,
1509
- recentSlotDurationMs: number
1577
+ recentSlotDurationMs: number,
1578
+ oracleAccounts?: AllOracleAccounts
1510
1579
  ): Promise<Map<Address, KaminoReserve>> {
1511
1580
  const reserves = await rpc
1512
1581
  .getProgramAccounts(programId, {
@@ -1537,7 +1606,7 @@ export async function getReservesForMarket(
1537
1606
  }
1538
1607
  return reserveAccount;
1539
1608
  });
1540
- const reservesAndOracles = await getTokenOracleData(rpc, deserializedReserves);
1609
+ const reservesAndOracles = await getTokenOracleData(rpc, deserializedReserves, oracleAccounts);
1541
1610
  const reservesByAddress = new Map<Address, KaminoReserve>();
1542
1611
  reservesAndOracles.forEach(([reserve, oracle], index) => {
1543
1612
  if (!oracle) {
@@ -1553,14 +1622,15 @@ export async function getSingleReserve(
1553
1622
  reservePk: Address,
1554
1623
  rpc: Rpc<KaminoReserveRpcApi>,
1555
1624
  recentSlotDurationMs: number,
1556
- reserveData?: Reserve
1625
+ reserveData?: Reserve,
1626
+ oracleAccounts?: AllOracleAccounts
1557
1627
  ): Promise<KaminoReserve> {
1558
1628
  const reserve = reserveData ?? (await Reserve.fetch(rpc, reservePk));
1559
1629
 
1560
1630
  if (reserve === null) {
1561
1631
  throw new Error(`Reserve account ${reservePk} does not exist`);
1562
1632
  }
1563
- const reservesAndOracles = await getTokenOracleData(rpc, [reserve]);
1633
+ const reservesAndOracles = await getTokenOracleData(rpc, [reserve], oracleAccounts);
1564
1634
  const [, oracle] = reservesAndOracles[0];
1565
1635
 
1566
1636
  if (!oracle) {
@@ -16,6 +16,7 @@ import {
16
16
  } from '@solana/kit';
17
17
  import Decimal from 'decimal.js';
18
18
  import {
19
+ AllOracleAccounts,
19
20
  DEFAULT_PUBLIC_KEY,
20
21
  getTokenOracleData,
21
22
  globalConfigPda,
@@ -102,14 +103,15 @@ export class KaminoReserve {
102
103
  address: Address,
103
104
  rpc: Rpc<KaminoReserveRpcApi>,
104
105
  recentSlotDurationMs: number,
105
- reserveState?: Reserve
106
+ reserveState?: Reserve,
107
+ oracleAccounts?: AllOracleAccounts
106
108
  ) {
107
109
  const reserve = reserveState ?? (await Reserve.fetch(rpc, address));
108
110
  if (reserve === null) {
109
111
  throw new Error(`Reserve account ${address} does not exist`);
110
112
  }
111
113
 
112
- const tokenOracleDataWithReserve = await getTokenOracleData(rpc, [reserve]);
114
+ const tokenOracleDataWithReserve = await getTokenOracleData(rpc, [reserve], oracleAccounts);
113
115
  if (!tokenOracleDataWithReserve[0]) {
114
116
  throw new Error('Token oracle data not found');
115
117
  }
@@ -23,6 +23,7 @@ import {
23
23
  TransactionSigner,
24
24
  } from '@solana/kit';
25
25
  import {
26
+ AllOracleAccounts,
26
27
  DEFAULT_PUBLIC_KEY,
27
28
  DEFAULT_RECENT_SLOT_DURATION_MS,
28
29
  getAssociatedTokenAddress,
@@ -124,6 +125,7 @@ import { getExtendLookupTableInstruction } from '@solana-program/address-lookup-
124
125
  import { Farms } from '@kamino-finance/farms-sdk';
125
126
  import { getFarmIncentives } from '@kamino-finance/farms-sdk/dist/utils/apy';
126
127
  import { computeReservesAllocation } from '../utils/vaultAllocation';
128
+ import { getReserveFarmRewardsAPY } from '../utils/farmUtils';
127
129
 
128
130
  export const kaminoVaultId = address('KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd');
129
131
  export const kaminoVaultStagingId = address('stKvQfwRsQiKnLtMNVLHKS3exFJmZFsgfzBPWHECUYK');
@@ -2064,19 +2066,21 @@ export class KaminoVaultClient {
2064
2066
 
2065
2067
  /**
2066
2068
  * This method calculates the token per share value. This will always change based on interest earned from the vault, but calculating it requires a bunch of rpc requests. Caching this for a short duration would be optimal
2067
- * @param vault - vault to calculate tokensPerShare for
2069
+ * @param vaultState - vault state to calculate tokensPerShare for
2068
2070
  * @param [slot] - the slot at which we retrieve the tokens per share. Optional. If not provided, the function will fetch the current slot
2069
2071
  * @param [vaultReservesMap] - hashmap from each reserve pubkey to the reserve state. Optional. If provided the function will be significantly faster as it will not have to fetch the reserves
2070
2072
  * @param [currentSlot] - the latest confirmed slot. Optional. If provided the function will be faster as it will not have to fetch the latest slot
2071
2073
  * @returns - token per share value
2072
2074
  */
2073
2075
  async getTokensPerShareSingleVault(
2074
- vault: KaminoVault,
2076
+ vaultOrState: KaminoVault | VaultState,
2075
2077
  slot?: Slot,
2076
2078
  vaultReservesMap?: Map<Address, KaminoReserve>,
2077
2079
  currentSlot?: Slot
2078
2080
  ): Promise<Decimal> {
2079
- const vaultState = await vault.getState(this.getConnection());
2081
+ // Determine if we have a KaminoVault or VaultState
2082
+ const vaultState = 'getState' in vaultOrState ? await vaultOrState.getState(this.getConnection()) : vaultOrState;
2083
+
2080
2084
  if (vaultState.sharesIssued.isZero()) {
2081
2085
  return new Decimal(0);
2082
2086
  }
@@ -2361,9 +2365,13 @@ export class KaminoVaultClient {
2361
2365
  /**
2362
2366
  * This will load the onchain state for all the reserves that the vaults have allocations for, deduplicating the reserves
2363
2367
  * @param vaults - the vault states to load reserves for
2368
+ * @param oracleAccounts (optional) all reserve oracle accounts, if not supplied will make an additional rpc call to fetch these accounts
2364
2369
  * @returns a hashmap from each reserve pubkey to the reserve state
2365
2370
  */
2366
- async loadVaultsReserves(vaults: VaultState[]): Promise<Map<Address, KaminoReserve>> {
2371
+ async loadVaultsReserves(
2372
+ vaults: VaultState[],
2373
+ oracleAccounts?: AllOracleAccounts
2374
+ ): Promise<Map<Address, KaminoReserve>> {
2367
2375
  const vaultReservesAddressesSet = new Set<Address>(vaults.flatMap((vault) => this.getVaultReserves(vault)));
2368
2376
  const vaultReservesAddresses = [...vaultReservesAddressesSet];
2369
2377
  const reserveAccounts = await this.getConnection()
@@ -2382,7 +2390,7 @@ export class KaminoVaultClient {
2382
2390
  return reserveAccount;
2383
2391
  });
2384
2392
 
2385
- const reservesAndOracles = await getTokenOracleData(this.getConnection(), deserializedReserves);
2393
+ const reservesAndOracles = await getTokenOracleData(this.getConnection(), deserializedReserves, oracleAccounts);
2386
2394
 
2387
2395
  const kaminoReserves = new Map<Address, KaminoReserve>();
2388
2396
 
@@ -2409,13 +2417,15 @@ export class KaminoVaultClient {
2409
2417
  * @param [slot] - the slot for which to retrieve the vault collaterals for. Optional. If not provided the function will fetch the current slot
2410
2418
  * @param [vaultReservesMap] - hashmap from each reserve pubkey to the reserve state. Optional. If provided the function will be significantly faster as it will not have to fetch the reserves
2411
2419
  * @param [kaminoMarkets] - a list of all the kamino markets. Optional. If provided the function will be significantly faster as it will not have to fetch the markets
2420
+ * @param oracleAccounts (optional) all reserve oracle accounts, if not supplied will make an additional rpc call to fetch these accounts
2412
2421
  * @returns a hashmap from each reserve pubkey to the market overview of the collaterals that can be used and the min and max loan to value ratio in that market
2413
2422
  */
2414
2423
  async getVaultCollaterals(
2415
2424
  vaultState: VaultState,
2416
2425
  slot: Slot,
2417
2426
  vaultReservesMap?: Map<Address, KaminoReserve>,
2418
- kaminoMarkets?: KaminoMarket[]
2427
+ kaminoMarkets?: KaminoMarket[],
2428
+ oracleAccounts?: AllOracleAccounts
2419
2429
  ): Promise<Map<Address, MarketOverview>> {
2420
2430
  const vaultReservesStateMap = vaultReservesMap ? vaultReservesMap : await this.loadVaultReserves(vaultState);
2421
2431
  const vaultReservesState: KaminoReserve[] = [];
@@ -2439,7 +2449,11 @@ export class KaminoVaultClient {
2439
2449
  const missingReservesStates = (await Reserve.fetchMultiple(this.getConnection(), [...missingReserves])).filter(
2440
2450
  (reserve) => reserve !== null
2441
2451
  );
2442
- const missingReservesAndOracles = await getTokenOracleData(this.getConnection(), missingReservesStates);
2452
+ const missingReservesAndOracles = await getTokenOracleData(
2453
+ this.getConnection(),
2454
+ missingReservesStates,
2455
+ oracleAccounts
2456
+ );
2443
2457
  missingReservesAndOracles.forEach(([reserve, oracle], index) => {
2444
2458
  const fetchedReserve = new KaminoReserve(
2445
2459
  reserve,
@@ -2638,16 +2652,16 @@ export class KaminoVaultClient {
2638
2652
  /**
2639
2653
  * This will return an VaultOverview object that encapsulates all the information about the vault, including the holdings, reserves details, theoretical APY, utilization ratio and total borrowed amount
2640
2654
  * @param vault - the kamino vault to get available liquidity to withdraw for
2641
- * @param price - the price of the token in the vault (e.g. USDC)
2655
+ * @param vaultTokenPrice - the price of the token in the vault (e.g. USDC)
2642
2656
  * @param [slot] - the slot for which to retrieve the vault overview for. Optional. If not provided the function will fetch the current slot
2643
2657
  * @param [vaultReservesMap] - hashmap from each reserve pubkey to the reserve state. Optional. If provided the function will be significantly faster as it will not have to fetch the reserves
2644
2658
  * @param [kaminoMarkets] - a list of all kamino markets. Optional. If provided the function will be significantly faster as it will not have to fetch the markets
2645
2659
  * @param [currentSlot] - the latest confirmed slot. Optional. If provided the function will be faster as it will not have to fetch the latest slot
2646
- * @returns an VaultOverview object with details about the tokens available and invested in the vault, denominated in tokens and USD
2660
+ * @returns an VaultOverview object with details about the tokens available and invested in the vault, denominated in tokens and USD, along sie APYs
2647
2661
  */
2648
2662
  async getVaultOverview(
2649
2663
  vault: VaultState,
2650
- price: Decimal,
2664
+ vaultTokenPrice: Decimal,
2651
2665
  slot?: Slot,
2652
2666
  vaultReservesMap?: Map<Address, KaminoReserve>,
2653
2667
  kaminoMarkets?: KaminoMarket[],
@@ -2655,30 +2669,34 @@ export class KaminoVaultClient {
2655
2669
  ): Promise<VaultOverview> {
2656
2670
  const vaultReservesState = vaultReservesMap ? vaultReservesMap : await this.loadVaultReserves(vault);
2657
2671
 
2658
- const vaultHoldingsWithUSDValuePromise = await this.getVaultHoldingsWithPrice(
2672
+ const vaultHoldingsWithUSDValuePromise = this.getVaultHoldingsWithPrice(
2659
2673
  vault,
2660
- price,
2674
+ vaultTokenPrice,
2661
2675
  slot,
2662
2676
  vaultReservesState,
2663
2677
  currentSlot
2664
2678
  );
2665
2679
 
2666
2680
  const slotForOverview = slot ? slot : await this.getConnection().getSlot().send();
2681
+ const farmsClient = new Farms(this.getConnection());
2667
2682
 
2668
- const vaultTheoreticalAPYPromise = await this.getVaultTheoreticalAPY(vault, slotForOverview, vaultReservesState);
2669
- const vaultActualAPYPromise = await this.getVaultActualAPY(vault, slotForOverview, vaultReservesState);
2670
- const totalInvestedAndBorrowedPromise = await this.getTotalBorrowedAndInvested(
2683
+ const vaultTheoreticalAPYPromise = this.getVaultTheoreticalAPY(vault, slotForOverview, vaultReservesState);
2684
+ const vaultActualAPYPromise = this.getVaultActualAPY(vault, slotForOverview, vaultReservesState);
2685
+ const totalInvestedAndBorrowedPromise = this.getTotalBorrowedAndInvested(
2671
2686
  vault,
2672
2687
  slotForOverview,
2673
2688
  vaultReservesState
2674
2689
  );
2675
- const vaultCollateralsPromise = await this.getVaultCollaterals(
2690
+ const vaultCollateralsPromise = this.getVaultCollaterals(vault, slotForOverview, vaultReservesState, kaminoMarkets);
2691
+ const reservesOverviewPromise = this.getVaultReservesDetails(vault, slotForOverview, vaultReservesState);
2692
+ const vaultFarmIncentivesPromise = this.getVaultRewardsAPY(vault, vaultTokenPrice, farmsClient, slotForOverview);
2693
+ const vaultReservesFarmIncentivesPromise = this.getVaultReservesFarmsIncentives(
2676
2694
  vault,
2695
+ vaultTokenPrice,
2696
+ farmsClient,
2677
2697
  slotForOverview,
2678
- vaultReservesState,
2679
- kaminoMarkets
2698
+ vaultReservesState
2680
2699
  );
2681
- const reservesOverviewPromise = await this.getVaultReservesDetails(vault, slotForOverview, vaultReservesState);
2682
2700
 
2683
2701
  // all the async part of the functions above just read the vaultReservesState which is read beforehand, so excepting vaultCollateralsPromise they should do no additional network calls
2684
2702
  const [
@@ -2688,6 +2706,8 @@ export class KaminoVaultClient {
2688
2706
  totalInvestedAndBorrowed,
2689
2707
  vaultCollaterals,
2690
2708
  reservesOverview,
2709
+ vaultFarmIncentives,
2710
+ vaultReservesFarmIncentives,
2691
2711
  ] = await Promise.all([
2692
2712
  vaultHoldingsWithUSDValuePromise,
2693
2713
  vaultTheoreticalAPYPromise,
@@ -2695,6 +2715,8 @@ export class KaminoVaultClient {
2695
2715
  totalInvestedAndBorrowedPromise,
2696
2716
  vaultCollateralsPromise,
2697
2717
  reservesOverviewPromise,
2718
+ vaultFarmIncentivesPromise,
2719
+ vaultReservesFarmIncentivesPromise,
2698
2720
  ]);
2699
2721
 
2700
2722
  return {
@@ -2703,11 +2725,13 @@ export class KaminoVaultClient {
2703
2725
  vaultCollaterals: vaultCollaterals,
2704
2726
  actualSupplyAPY: vaultActualAPYs,
2705
2727
  theoreticalSupplyAPY: vaultTheoreticalAPYs,
2728
+ vaultFarmIncentives: vaultFarmIncentives,
2729
+ reservesFarmsIncentives: vaultReservesFarmIncentives,
2706
2730
  totalBorrowed: totalInvestedAndBorrowed.totalBorrowed,
2707
- totalBorrowedUSD: totalInvestedAndBorrowed.totalBorrowed.mul(price),
2731
+ totalBorrowedUSD: totalInvestedAndBorrowed.totalBorrowed.mul(vaultTokenPrice),
2708
2732
  utilizationRatio: totalInvestedAndBorrowed.utilizationRatio,
2709
2733
  totalSupplied: totalInvestedAndBorrowed.totalInvested,
2710
- totalSuppliedUSD: totalInvestedAndBorrowed.totalInvested.mul(price),
2734
+ totalSuppliedUSD: totalInvestedAndBorrowed.totalInvested.mul(vaultTokenPrice),
2711
2735
  };
2712
2736
  }
2713
2737
 
@@ -3021,11 +3045,18 @@ export class KaminoVaultClient {
3021
3045
  * Read the APY of the farm built on top of the vault (farm in vaultState.vaultFarm)
3022
3046
  * @param vault - the vault to read the farm APY for
3023
3047
  * @param vaultTokenPrice - the price of the vault token in USD (e.g. 1.0 for USDC)
3048
+ * @param [farmsClient] - the farms client to use. Optional. If not provided, the function will create a new one
3024
3049
  * @param [slot] - the slot to read the farm APY for. Optional. If not provided, the function will read the current slot
3025
3050
  * @returns the APY of the farm built on top of the vault
3026
3051
  */
3027
- async getVaultRewardsAPY(vault: KaminoVault, vaultTokenPrice: Decimal, slot?: Slot): Promise<FarmIncentives> {
3028
- const vaultState = await vault.getState(this.getConnection());
3052
+ async getVaultRewardsAPY(
3053
+ vaultOrState: KaminoVault | VaultState,
3054
+ vaultTokenPrice: Decimal,
3055
+ farmsClient?: Farms,
3056
+ slot?: Slot
3057
+ ): Promise<FarmIncentives> {
3058
+ // Determine if we have a KaminoVault or VaultState
3059
+ const vaultState = 'getState' in vaultOrState ? await vaultOrState.getState(this.getConnection()) : vaultOrState;
3029
3060
  if (vaultState.vaultFarm === DEFAULT_PUBLIC_KEY) {
3030
3061
  return {
3031
3062
  incentivesStats: [],
@@ -3033,12 +3064,74 @@ export class KaminoVaultClient {
3033
3064
  };
3034
3065
  }
3035
3066
 
3036
- const tokensPerShare = await this.getTokensPerShareSingleVault(vault, slot);
3067
+ const tokensPerShare = await this.getTokensPerShareSingleVault(vaultState, slot);
3037
3068
  const sharePrice = tokensPerShare.mul(vaultTokenPrice);
3038
3069
  const stakedTokenMintDecimals = vaultState.sharesMintDecimals.toNumber();
3039
3070
 
3040
- const farmsClient = new Farms(this.getConnection());
3041
- return getFarmIncentives(farmsClient, vaultState.vaultFarm, sharePrice, stakedTokenMintDecimals);
3071
+ const kFarmsClient = farmsClient ? farmsClient : new Farms(this.getConnection());
3072
+ return getFarmIncentives(kFarmsClient, vaultState.vaultFarm, sharePrice, stakedTokenMintDecimals);
3073
+ }
3074
+
3075
+ async getVaultReservesFarmsIncentives(
3076
+ vaultOrState: KaminoVault | VaultState,
3077
+ vaultTokenPrice: Decimal,
3078
+ farmsClient?: Farms,
3079
+ slot?: Slot,
3080
+ vaultReservesMap?: Map<Address, KaminoReserve>
3081
+ ): Promise<VaultReservesFarmsIncentives> {
3082
+ const vaultState = 'getState' in vaultOrState ? await vaultOrState.getState(this.getConnection()) : vaultOrState;
3083
+
3084
+ const vaultReservesState = vaultReservesMap ? vaultReservesMap : await this.loadVaultReserves(vaultState);
3085
+ const currentSlot = slot ? slot : await this.getConnection().getSlot({ commitment: 'confirmed' }).send();
3086
+
3087
+ const holdings = await this.getVaultHoldings(vaultState, currentSlot, vaultReservesState);
3088
+
3089
+ const vaultReservesAddresses = vaultState.vaultAllocationStrategy.map(
3090
+ (allocationStrategy) => allocationStrategy.reserve
3091
+ );
3092
+
3093
+ const vaultReservesFarmsIncentives = new Map<Address, FarmIncentives>();
3094
+ let totalIncentivesApy = new Decimal(0);
3095
+
3096
+ const kFarmsClient = farmsClient ? farmsClient : new Farms(this.getConnection());
3097
+ for (const reserveAddress of vaultReservesAddresses) {
3098
+ if (reserveAddress === DEFAULT_PUBLIC_KEY) {
3099
+ continue;
3100
+ }
3101
+
3102
+ const reserveState = vaultReservesState.get(reserveAddress);
3103
+ if (reserveState === undefined) {
3104
+ console.log(`Reserve to read farm incentives for not found: ${reserveAddress}`);
3105
+ vaultReservesFarmsIncentives.set(reserveAddress, {
3106
+ incentivesStats: [],
3107
+ totalIncentivesApy: 0,
3108
+ });
3109
+ continue;
3110
+ }
3111
+
3112
+ const reserveFarmIncentives = await getReserveFarmRewardsAPY(
3113
+ this._rpc,
3114
+ this.recentSlotDurationMs,
3115
+ reserveAddress,
3116
+ vaultTokenPrice,
3117
+ this._kaminoLendProgramId,
3118
+ kFarmsClient,
3119
+ currentSlot,
3120
+ reserveState.state
3121
+ );
3122
+ vaultReservesFarmsIncentives.set(reserveAddress, reserveFarmIncentives.collateralFarmIncentives);
3123
+
3124
+ const investedInReserve = holdings.investedInReserves.get(reserveAddress);
3125
+ const weightedReserveAPY = new Decimal(reserveFarmIncentives.collateralFarmIncentives.totalIncentivesApy)
3126
+ .mul(investedInReserve ?? 0)
3127
+ .div(holdings.totalAUMIncludingFees);
3128
+ totalIncentivesApy = totalIncentivesApy.add(weightedReserveAPY);
3129
+ }
3130
+
3131
+ return {
3132
+ reserveFarmsIncentives: vaultReservesFarmsIncentives,
3133
+ totalIncentivesAPY: totalIncentivesApy,
3134
+ };
3042
3135
  }
3043
3136
 
3044
3137
  private appendRemainingAccountsForVaultReserves(
@@ -3275,6 +3368,8 @@ export type VaultOverview = {
3275
3368
  vaultCollaterals: Map<Address, MarketOverview>;
3276
3369
  theoreticalSupplyAPY: APYs;
3277
3370
  actualSupplyAPY: APYs;
3371
+ vaultFarmIncentives: FarmIncentives;
3372
+ reservesFarmsIncentives: VaultReservesFarmsIncentives;
3278
3373
  totalBorrowed: Decimal;
3279
3374
  totalBorrowedUSD: Decimal;
3280
3375
  totalSupplied: Decimal;
@@ -3282,6 +3377,11 @@ export type VaultOverview = {
3282
3377
  utilizationRatio: Decimal;
3283
3378
  };
3284
3379
 
3380
+ export type VaultReservesFarmsIncentives = {
3381
+ reserveFarmsIncentives: Map<Address, FarmIncentives>;
3382
+ totalIncentivesAPY: Decimal;
3383
+ };
3384
+
3285
3385
  export type VaultFeesPct = {
3286
3386
  managementFeePct: Decimal;
3287
3387
  performanceFeePct: Decimal;
@@ -1,5 +1,6 @@
1
1
  import { Address, generateKeyPairSigner, TransactionSigner } from '@solana/kit';
2
2
  import {
3
+ DEFAULT_RECENT_SLOT_DURATION_MS,
3
4
  initFarmsForReserve as initFarmsForReserveIx,
4
5
  KaminoMarket,
5
6
  lendingMarketAuthPda,
@@ -11,7 +12,6 @@ import { getCreateAccountInstruction, SYSTEM_PROGRAM_ADDRESS } from '@solana-pro
11
12
  import { SYSVAR_RENT_ADDRESS } from '@solana/sysvars';
12
13
  import { CliEnv, SendTxMode } from '../tx/CliEnv';
13
14
  import { processTx } from '../tx/processor';
14
- import { DEFAULT_RECENT_SLOT_DURATION_MS } from '@kamino-finance/klend-sdk';
15
15
 
16
16
  export async function initFarmsForReserve(
17
17
  env: CliEnv,
@@ -1318,6 +1318,10 @@ async function main() {
1318
1318
  );
1319
1319
 
1320
1320
  console.log('vaultOverview', vaultOverview);
1321
+ vaultOverview.reservesFarmsIncentives.reserveFarmsIncentives.forEach((incentive, reserveAddress) => {
1322
+ console.log('reserve ', reserveAddress);
1323
+ console.log('reserve incentive', incentive);
1324
+ });
1321
1325
  });
1322
1326
 
1323
1327
  commands
@@ -0,0 +1,73 @@
1
+ import { Address, Rpc, Slot, SolanaRpcApi } from '@solana/kit';
2
+ import { Decimal } from 'decimal.js';
3
+ import { FarmIncentives, Farms } from '@kamino-finance/farms-sdk';
4
+ import { getFarmIncentives } from '@kamino-finance/farms-sdk/dist/utils/apy';
5
+ import { DEFAULT_PUBLIC_KEY } from '@kamino-finance/farms-sdk';
6
+ import { Reserve } from '../@codegen/klend/accounts';
7
+ import { KaminoReserve } from '../lib';
8
+ import { getMintDecimals } from '@kamino-finance/kliquidity-sdk';
9
+
10
+ export interface ReserveIncentives {
11
+ collateralFarmIncentives: FarmIncentives;
12
+ debtFarmIncentives: FarmIncentives;
13
+ }
14
+
15
+ export async function getReserveFarmRewardsAPY(
16
+ rpc: Rpc<SolanaRpcApi>,
17
+ recentSlotDurationMs: number,
18
+ reserve: Address,
19
+ reserveLiquidityTokenPrice: Decimal,
20
+ kaminoLendProgramId: Address,
21
+ farmsClient: Farms,
22
+ slot: Slot,
23
+ reserveState?: Reserve,
24
+ cTokenMintDecimals?: number
25
+ ): Promise<ReserveIncentives> {
26
+ const reserveIncentives: ReserveIncentives = {
27
+ collateralFarmIncentives: {
28
+ incentivesStats: [],
29
+ totalIncentivesApy: 0,
30
+ },
31
+ debtFarmIncentives: {
32
+ incentivesStats: [],
33
+ totalIncentivesApy: 0,
34
+ },
35
+ };
36
+
37
+ const reserveAccount = reserveState ? reserveState : await Reserve.fetch(rpc, reserve, kaminoLendProgramId);
38
+ if (!reserveAccount) {
39
+ throw new Error(`Reserve ${reserve} not found`);
40
+ }
41
+
42
+ const kaminoReserve = await KaminoReserve.initializeFromAddress(reserve, rpc, recentSlotDurationMs, reserveAccount);
43
+
44
+ const farmCollateral = kaminoReserve.state.farmCollateral;
45
+ const farmDebt = kaminoReserve.state.farmDebt;
46
+
47
+ const stakedTokenMintDecimals = kaminoReserve.getMintDecimals();
48
+ const reserveCtokenPrice = reserveLiquidityTokenPrice.div(kaminoReserve.getEstimatedCollateralExchangeRate(slot, 0));
49
+ const cTokenMint = kaminoReserve.getCTokenMint();
50
+ const cTokenDecimals = cTokenMintDecimals ? cTokenMintDecimals : await getMintDecimals(rpc, cTokenMint);
51
+
52
+ if (farmCollateral !== DEFAULT_PUBLIC_KEY) {
53
+ const farmIncentivesCollateral = await getFarmIncentives(
54
+ farmsClient,
55
+ farmCollateral,
56
+ reserveCtokenPrice,
57
+ cTokenDecimals
58
+ );
59
+ reserveIncentives.collateralFarmIncentives = farmIncentivesCollateral;
60
+ }
61
+
62
+ if (farmDebt !== DEFAULT_PUBLIC_KEY) {
63
+ const farmIncentivesDebt = await getFarmIncentives(
64
+ farmsClient,
65
+ farmDebt,
66
+ reserveLiquidityTokenPrice,
67
+ stakedTokenMintDecimals
68
+ );
69
+ reserveIncentives.debtFarmIncentives = farmIncentivesDebt;
70
+ }
71
+
72
+ return reserveIncentives;
73
+ }
@@ -105,9 +105,10 @@ export function getTokenOracleDataSync(allOracleAccounts: AllOracleAccounts, res
105
105
  // TODO: Add freshness of the latest price to match sc logic
106
106
  export async function getTokenOracleData(
107
107
  rpc: Rpc<GetMultipleAccountsApi>,
108
- reserves: Reserve[]
108
+ reserves: Reserve[],
109
+ oracleAccounts?: AllOracleAccounts
109
110
  ): Promise<Array<[Reserve, TokenOracleData | undefined]>> {
110
- const allOracleAccounts = await getAllOracleAccounts(rpc, reserves);
111
+ const allOracleAccounts = oracleAccounts ?? (await getAllOracleAccounts(rpc, reserves));
111
112
  return getTokenOracleDataSync(allOracleAccounts, reserves);
112
113
  }
113
114