@kamino-finance/klend-sdk 5.1.8 → 5.1.9

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.
@@ -13,9 +13,9 @@ import {
13
13
  import { TOKEN_PROGRAM_ID, unpackAccount } from '@solana/spl-token';
14
14
  import {
15
15
  getAssociatedTokenAddress,
16
- getAtasWithCreateIxnsIfMissing,
17
16
  getDepositWsolIxns,
18
17
  getTokenOracleData,
18
+ KaminoMarket,
19
19
  KaminoReserve,
20
20
  lamportsToDecimal,
21
21
  LendingMarket,
@@ -56,9 +56,9 @@ import { withdraw } from '../idl_codegen_kamino_vault/instructions';
56
56
  import { PROGRAM_ID } from '../idl_codegen/programId';
57
57
  import { DEFAULT_RECENT_SLOT_DURATION_MS, ReserveWithAddress } from './reserve';
58
58
  import { Fraction } from './fraction';
59
- import { lendingMarketAuthPda } from '../utils';
59
+ import { createAtasIdempotent, lendingMarketAuthPda } from '../utils';
60
60
  import bs58 from 'bs58';
61
- import { getProgramAccounts } from '../utils/rpc';
61
+ import { getAccountOwner, getProgramAccounts } from '../utils/rpc';
62
62
 
63
63
  export const kaminoVaultId = new PublicKey('kvauTFR8qm1dhniz6pYuBZkuene3Hfrs1VQhVRgCNrr');
64
64
  export const kaminoVaultStagingId = new PublicKey('STkvh7ostar39Fwr4uZKASs1RNNuYMFMTsE77FiRsL2');
@@ -306,13 +306,12 @@ export class KaminoVaultClient {
306
306
  */
307
307
  async withdrawPendingFeesIxs(vault: KaminoVault, slot: number): Promise<TransactionInstruction[]> {
308
308
  const vaultState: VaultState = await vault.getState(this.getConnection());
309
- const { atas, createAtaIxs } = await getAtasWithCreateIxnsIfMissing(this._connection, vaultState.adminAuthority, [
309
+ const [{ ata: adminTokenAta, createAtaIx }] = createAtasIdempotent(vaultState.adminAuthority, [
310
310
  {
311
311
  mint: vaultState.tokenMint,
312
312
  tokenProgram: TOKEN_PROGRAM_ID,
313
313
  },
314
314
  ]);
315
- const adminTokenAta = atas[0];
316
315
 
317
316
  const tokensToWithdraw = new Fraction(vaultState.pendingFeesSf).toDecimal();
318
317
  let tokenLeftToWithdraw = tokensToWithdraw;
@@ -371,7 +370,7 @@ export class KaminoVaultClient {
371
370
  })
372
371
  );
373
372
 
374
- return [...createAtaIxs, ...withdrawIxns];
373
+ return [createAtaIx, ...withdrawIxns];
375
374
  }
376
375
 
377
376
  // async closeVaultIx(vault: KaminoVault): Promise<TransactionInstruction> {
@@ -390,6 +389,7 @@ export class KaminoVaultClient {
390
389
  * @param user - user to deposit
391
390
  * @param vault - vault to deposit into
392
391
  * @param tokenAmount - token amount to be deposited, in decimals (will be converted in lamports)
392
+ * @param tokenProgramIDOverride - optional param; should be passed if token to be deposited is token22
393
393
  * @param vaultReservesMap - optional parameter; a hashmap from each reserve pubkey to the reserve state. If provided the function will be significantly faster as it will not have to fetch the reserves
394
394
  * @returns - an array of instructions to be used to be executed
395
395
  */
@@ -397,42 +397,38 @@ export class KaminoVaultClient {
397
397
  user: PublicKey,
398
398
  vault: KaminoVault,
399
399
  tokenAmount: Decimal,
400
+ tokenProgramIDOverride?: PublicKey,
400
401
  vaultReservesMap?: PubkeyHashMap<PublicKey, KaminoReserve>
401
402
  ): Promise<TransactionInstruction[]> {
402
403
  const vaultState = await vault.getState(this._connection);
403
404
 
404
- const userTokenAta = getAssociatedTokenAddress(vaultState.tokenMint, user);
405
+ const tokenProgramID = tokenProgramIDOverride ? tokenProgramIDOverride : TOKEN_PROGRAM_ID;
406
+ const userTokenAta = getAssociatedTokenAddress(vaultState.tokenMint, user, true, tokenProgramID);
405
407
  const createAtasIxns: TransactionInstruction[] = [];
406
408
  const closeAtasIxns: TransactionInstruction[] = [];
407
409
  if (vaultState.tokenMint.equals(WRAPPED_SOL_MINT)) {
408
- const { atas: wsolAta, createAtaIxs: createWsolAtaIxns } = await getAtasWithCreateIxnsIfMissing(
409
- this._connection,
410
- user,
411
- [
412
- {
413
- mint: WRAPPED_SOL_MINT,
414
- tokenProgram: TOKEN_PROGRAM_ID,
415
- },
416
- ]
417
- );
418
- createAtasIxns.push(...createWsolAtaIxns);
410
+ const [{ ata: wsolAta, createAtaIx: createWsolAtaIxn }] = createAtasIdempotent(user, [
411
+ {
412
+ mint: WRAPPED_SOL_MINT,
413
+ tokenProgram: TOKEN_PROGRAM_ID,
414
+ },
415
+ ]);
416
+ createAtasIxns.push(createWsolAtaIxn);
419
417
  const depositWsolIxn = getDepositWsolIxns(
420
418
  user,
421
- wsolAta[0],
419
+ wsolAta,
422
420
  numberToLamportsDecimal(tokenAmount, vaultState.tokenMintDecimals.toNumber()).ceil()
423
421
  );
424
422
  createAtasIxns.push(...depositWsolIxn);
425
423
  }
426
424
 
427
- const { atas, createAtaIxs: createSharesAtaIxns } = await getAtasWithCreateIxnsIfMissing(this._connection, user, [
425
+ const [{ ata: userSharesAta, createAtaIx: createSharesAtaIxns }] = createAtasIdempotent(user, [
428
426
  {
429
427
  mint: vaultState.sharesMint,
430
428
  tokenProgram: TOKEN_PROGRAM_ID,
431
429
  },
432
430
  ]);
433
- createAtasIxns.push(...createSharesAtaIxns);
434
-
435
- const userSharesAta = atas[0];
431
+ createAtasIxns.push(createSharesAtaIxns);
436
432
 
437
433
  const depoistAccounts: DepositAccounts = {
438
434
  user: user,
@@ -443,7 +439,7 @@ export class KaminoVaultClient {
443
439
  sharesMint: vaultState.sharesMint,
444
440
  tokenAta: userTokenAta,
445
441
  userSharesAta: userSharesAta,
446
- tokenProgram: TOKEN_PROGRAM_ID,
442
+ tokenProgram: tokenProgramID,
447
443
  instructionSysvarAccount: SYSVAR_INSTRUCTIONS_PUBKEY,
448
444
  klendProgram: this._kaminoLendProgramId,
449
445
  sharesTokenProgram: TOKEN_PROGRAM_ID,
@@ -496,13 +492,12 @@ export class KaminoVaultClient {
496
492
  const vaultState = await vault.getState(this._connection);
497
493
 
498
494
  const userSharesAta = getAssociatedTokenAddress(vaultState.sharesMint, user);
499
- const { atas, createAtaIxs } = await getAtasWithCreateIxnsIfMissing(this._connection, user, [
495
+ const [{ ata: userTokenAta, createAtaIx }] = createAtasIdempotent(user, [
500
496
  {
501
497
  mint: vaultState.tokenMint,
502
498
  tokenProgram: TOKEN_PROGRAM_ID,
503
499
  },
504
500
  ]);
505
- const userTokenAta = atas[0];
506
501
 
507
502
  const tokensToWithdraw = shareAmount.div(await this.getTokensPerShareSingleVault(vault, slot));
508
503
  let tokenLeftToWithdraw = tokensToWithdraw;
@@ -569,7 +564,7 @@ export class KaminoVaultClient {
569
564
  })
570
565
  );
571
566
 
572
- return [...createAtaIxs, ...withdrawIxns];
567
+ return [createAtaIx, ...withdrawIxns];
573
568
  }
574
569
 
575
570
  /**
@@ -580,18 +575,20 @@ export class KaminoVaultClient {
580
575
  */
581
576
  async investAllReservesIxs(payer: PublicKey, vault: KaminoVault): Promise<TransactionInstruction[]> {
582
577
  //TODO: Order invest ixns by - invest that removes first, then invest that adds
583
-
584
578
  const vaultState = await vault.getState(this._connection);
585
579
  const vaultReserves = this.getVaultReserves(vaultState);
586
- const investIxns: TransactionInstruction[] = [];
580
+ const investIxnsPromises: Promise<TransactionInstruction[]>[] = [];
587
581
  for (const reserve of vaultReserves) {
588
582
  const reserveState = await Reserve.fetch(this._connection, reserve, this._kaminoLendProgramId);
589
583
  if (reserveState === null) {
590
584
  throw new Error(`Reserve ${reserve.toBase58()} not found`);
591
585
  }
592
- investIxns.push(await this.investSingleReserveIxs(payer, vault, { address: reserve, state: reserveState }));
586
+ const investIxsPromise = this.investSingleReserveIxs(payer, vault, { address: reserve, state: reserveState });
587
+ investIxnsPromises.push(investIxsPromise);
593
588
  }
594
589
 
590
+ const investIxns = await Promise.all(investIxnsPromises).then((ixns) => ixns.flat());
591
+
595
592
  return investIxns;
596
593
  }
597
594
 
@@ -606,13 +603,16 @@ export class KaminoVaultClient {
606
603
  payer: PublicKey,
607
604
  vault: KaminoVault,
608
605
  reserve: ReserveWithAddress
609
- ): Promise<TransactionInstruction> {
606
+ ): Promise<TransactionInstruction[]> {
610
607
  const vaultState = await vault.getState(this._connection);
611
608
 
612
609
  const cTokenVault = getCTokenVaultPda(vault.address, reserve.address, this._kaminoVaultProgramId);
613
610
  const lendingMarketAuth = lendingMarketAuthPda(reserve.state.lendingMarket, this._kaminoLendProgramId)[0];
614
611
 
615
- const payerTokenAta = getAssociatedTokenAddress(vaultState.tokenMint, payer);
612
+ const tokenProgram = await getAccountOwner(this._connection, vaultState.tokenMint);
613
+ const [{ ata: payerTokenAta, createAtaIx }] = createAtasIdempotent(payer, [
614
+ { mint: vaultState.tokenMint, tokenProgram },
615
+ ]);
616
616
 
617
617
  const investAccounts: InvestAccounts = {
618
618
  payer,
@@ -628,7 +628,7 @@ export class KaminoVaultClient {
628
628
  reserveCollateralMint: reserve.state.collateral.mintPubkey,
629
629
  klendProgram: this._kaminoLendProgramId,
630
630
  instructionSysvarAccount: SYSVAR_INSTRUCTIONS_PUBKEY,
631
- tokenProgram: TOKEN_PROGRAM_ID,
631
+ tokenProgram: tokenProgram,
632
632
  payerTokenAccount: payerTokenAta,
633
633
  tokenMint: vaultState.tokenMint,
634
634
  reserveCollateralTokenProgram: TOKEN_PROGRAM_ID,
@@ -643,7 +643,7 @@ export class KaminoVaultClient {
643
643
  });
644
644
  investIx.keys = investIx.keys.concat(vaultReservesAccountMetas);
645
645
 
646
- return investIx;
646
+ return [createAtaIx, investIx];
647
647
  }
648
648
 
649
649
  private async withdrawIxn(
@@ -1047,6 +1047,57 @@ export class KaminoVaultClient {
1047
1047
  return kaminoReserves;
1048
1048
  }
1049
1049
 
1050
+ /**
1051
+ * This will retrieve all the tokens that can be use as collateral by the users who borrow the token in the vault alongside details about the min and max loan to value ratio
1052
+ * @param vaultState - the vault state to load reserves for
1053
+ *
1054
+ * @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
1055
+ */
1056
+ async getVaultCollaterals(vaultState: VaultState, slot: number): Promise<PubkeyHashMap<PublicKey, MarketOverview>> {
1057
+ const vaultReservesState = Array.from((await this.loadVaultReserves(vaultState)).values());
1058
+
1059
+ const vaultCollateralsPerReserve: PubkeyHashMap<PublicKey, MarketOverview> = new PubkeyHashMap();
1060
+
1061
+ for (const reserve of vaultReservesState) {
1062
+ const lendingMarket = await KaminoMarket.load(this._connection, reserve.state.lendingMarket, slot);
1063
+ if (!lendingMarket) {
1064
+ throw Error(`Could not fetch lending market ${reserve.state.lendingMarket.toBase58()}`);
1065
+ }
1066
+
1067
+ const marketReserves = lendingMarket.getReserves();
1068
+ const marketOverview: MarketOverview = {
1069
+ address: reserve.state.lendingMarket,
1070
+ reservesAsCollateral: [],
1071
+ minLTVPct: new Decimal(0),
1072
+ maxLTVPct: new Decimal(100),
1073
+ };
1074
+
1075
+ marketReserves
1076
+ .filter((marketReserve) => {
1077
+ return (
1078
+ marketReserve.state.config.liquidationThresholdPct > 0 && !marketReserve.address.equals(reserve.address)
1079
+ );
1080
+ })
1081
+ .map((filteredReserve) => {
1082
+ const reserveAsCollateral: ReserveAsCollateral = {
1083
+ mint: filteredReserve.getLiquidityMint(),
1084
+ liquidationLTVPct: new Decimal(filteredReserve.state.config.liquidationThresholdPct),
1085
+ };
1086
+ marketOverview.reservesAsCollateral.push(reserveAsCollateral);
1087
+ if (reserveAsCollateral.liquidationLTVPct.lt(marketOverview.minLTVPct) || marketOverview.minLTVPct.eq(0)) {
1088
+ marketOverview.minLTVPct = reserveAsCollateral.liquidationLTVPct;
1089
+ }
1090
+ if (reserveAsCollateral.liquidationLTVPct.gt(marketOverview.maxLTVPct) || marketOverview.maxLTVPct.eq(0)) {
1091
+ marketOverview.maxLTVPct = reserveAsCollateral.liquidationLTVPct;
1092
+ }
1093
+ });
1094
+
1095
+ vaultCollateralsPerReserve.set(reserve.address, marketOverview);
1096
+ }
1097
+
1098
+ return vaultCollateralsPerReserve;
1099
+ }
1100
+
1050
1101
  /**
1051
1102
  * This will return an VaultHoldings object which contains the amount available (uninvested) in vault, total amount invested in reseves and a breakdown of the amount invested in each reserve
1052
1103
  * @param vault - the kamino vault to get available liquidity to withdraw for
@@ -1338,3 +1389,15 @@ export type ReserveOverview = {
1338
1389
  liquidationThresholdPct: Decimal;
1339
1390
  borrowedAmount: Decimal;
1340
1391
  };
1392
+
1393
+ export type MarketOverview = {
1394
+ address: PublicKey;
1395
+ reservesAsCollateral: ReserveAsCollateral[]; // this MarketOverview has the reserve the caller calls for as the debt reserve and all the others as collateral reserves, so the debt reserve is not included here
1396
+ minLTVPct: Decimal;
1397
+ maxLTVPct: Decimal;
1398
+ };
1399
+
1400
+ export type ReserveAsCollateral = {
1401
+ mint: PublicKey;
1402
+ liquidationLTVPct: Decimal;
1403
+ };
@@ -532,6 +532,78 @@ async function main() {
532
532
  mode === 'execute' && console.log('User withdraw:', depositSig);
533
533
  });
534
534
 
535
+ commands
536
+ .command('invest-all-reserves')
537
+ .requiredOption('--vault <string>', 'Vault address')
538
+ .requiredOption(
539
+ `--mode <string>`,
540
+ 'simulate - to print txn simulation, inspect - to get txn simulation in explorer, execute - execute txn, multisig - to get bs58 txn for multisig usage'
541
+ )
542
+ .option(`--staging`, 'If true, will use the staging programs')
543
+ .option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
544
+ .action(async ({ vault, mode, staging, multisig }) => {
545
+ const env = initializeClient(mode === 'multisig', staging);
546
+ const vaultAddress = new PublicKey(vault);
547
+
548
+ if (mode === 'multisig' && !multisig) {
549
+ throw new Error('If using multisig mode, multisig is required');
550
+ }
551
+
552
+ const kaminoManager = new KaminoManager(env.connection, env.kLendProgramId, env.kVaultProgramId);
553
+
554
+ const kaminoVault = new KaminoVault(vaultAddress, undefined, env.kVaultProgramId);
555
+ const instructions = await kaminoManager.investAllReserves(env.payer.publicKey, kaminoVault);
556
+
557
+ for (let i = 0; i < instructions.length; i++) {
558
+ const txInstructions: TransactionInstruction[] = [];
559
+ txInstructions.push(instructions[i]);
560
+ const investReserveSig = await processTxn(env.client, env.payer, txInstructions, mode, 2500, [], 400000);
561
+
562
+ mode === 'execute' && console.log('Reserve invested:', investReserveSig);
563
+ }
564
+ });
565
+
566
+ commands
567
+ .command('invest-single-reserve')
568
+ .requiredOption('--vault <string>', 'Vault address')
569
+ .requiredOption('--reserve <string>', 'Reserve address')
570
+ .requiredOption(
571
+ `--mode <string>`,
572
+ 'simulate - to print txn simulation, inspect - to get txn simulation in explorer, execute - execute txn, multisig - to get bs58 txn for multisig usage'
573
+ )
574
+ .option(`--staging`, 'If true, will use the staging programs')
575
+ .option(`--multisig <string>`, 'If using multisig mode this is required, otherwise will be ignored')
576
+ .action(async ({ vault, reserve, mode, staging, multisig }) => {
577
+ const env = initializeClient(mode === 'multisig', staging);
578
+ const vaultAddress = new PublicKey(vault);
579
+
580
+ if (mode === 'multisig' && !multisig) {
581
+ throw new Error('If using multisig mode, multisig is required');
582
+ }
583
+
584
+ const kaminoManager = new KaminoManager(env.connection, env.kLendProgramId, env.kVaultProgramId);
585
+
586
+ const kaminoVault = new KaminoVault(vaultAddress, undefined, env.kVaultProgramId);
587
+ const reserveState = await Reserve.fetch(env.connection, new PublicKey(reserve), env.kLendProgramId);
588
+ if (!reserveState) {
589
+ throw new Error('Reserve not found');
590
+ }
591
+
592
+ const reserveWithAddress: ReserveWithAddress = {
593
+ address: new PublicKey(reserve),
594
+ state: reserveState,
595
+ };
596
+
597
+ const instructions = await kaminoManager.investSingleReserve(
598
+ env.payer.publicKey,
599
+ kaminoVault,
600
+ reserveWithAddress
601
+ );
602
+ const investReserveSig = await processTxn(env.client, env.payer, instructions, mode, 2500, [], 400_000);
603
+
604
+ mode === 'execute' && console.log('Reserve invested:', investReserveSig);
605
+ });
606
+
535
607
  // commands
536
608
  // .command('close-vault')
537
609
  // .requiredOption('--vault <string>', 'Vault address')
@@ -549,6 +621,28 @@ async function main() {
549
621
  // console.log('Vault closed:', closeVaultSig);
550
622
  // });
551
623
 
624
+ commands
625
+ .command('get-vault-colls')
626
+ .requiredOption('--vault <string>', 'Vault address')
627
+ .action(async ({ vault }) => {
628
+ const env = initializeClient(false, false);
629
+
630
+ const kaminoManager = new KaminoManager(env.connection, env.kLendProgramId, env.kVaultProgramId);
631
+
632
+ const vaultAddress = new PublicKey(vault);
633
+ const vaultState = await new KaminoVault(vaultAddress, undefined, env.kVaultProgramId).getState(env.connection);
634
+ const vaultCollaterals = await kaminoManager.getVaultCollaterals(
635
+ vaultState,
636
+ await env.connection.getSlot('confirmed')
637
+ );
638
+ vaultCollaterals.forEach((collateral) => {
639
+ console.log('reserve ', collateral.address);
640
+ console.log('market overview', collateral.reservesAsCollateral);
641
+ console.log('min LTV', collateral.minLTVPct);
642
+ console.log('max LTV', collateral.maxLTVPct);
643
+ });
644
+ });
645
+
552
646
  commands.command('get-oracle-mappings').action(async () => {
553
647
  const env = initializeClient(false, false);
554
648
  const kaminoManager = new KaminoManager(env.connection, env.kLendProgramId, env.kVaultProgramId);
@@ -851,7 +945,11 @@ async function processTxn(
851
945
  const simulation = await web3Client.sendConnection.simulateTransaction(
852
946
  new VersionedTransaction(tx.compileMessage())
853
947
  );
854
- console.log('Simulation: \n' + simulation.value.logs);
948
+ if (simulation.value.logs && simulation.value.logs.length > 0) {
949
+ console.log('Simulation: \n' + simulation.value.logs);
950
+ } else {
951
+ console.log('Simulation failed: \n' + simulation);
952
+ }
855
953
  } else if (mode === 'inspect') {
856
954
  console.log(
857
955
  'Tx in B64',