UNPKG

@kamino-finance/klend-sdk

Version:

Typescript SDK for interacting with the Kamino Lending (klend) protocol

524 lines (471 loc) 19.4 kB
import { Command } from 'commander'; import { DEFAULT_RECENT_SLOT_DURATION_MS, KaminoAction, KaminoMarket, KaminoObligation, PROGRAM_ID, STAGING_PROGRAM_ID, getAllUserMetadatasWithFilter, getProgramId, toJson, getAllObligationAccounts, getAllReserveAccounts, getAllLendingMarketAccounts, KaminoManager, } from './lib'; import * as fs from 'fs'; import { Connection, GetProgramAccountsFilter, Keypair, PublicKey } from '@solana/web3.js'; import { BN } from '@coral-xyz/anchor'; import { Reserve } from './idl_codegen/accounts'; import { buildAndSendTxnWithLogs, buildVersionedTransaction } from './utils/instruction'; import { VanillaObligation } from './utils/ObligationType'; import { getMedianSlotDurationInMsFromLastEpochs, parseTokenSymbol } from './classes/utils'; import { Env, initEnv } from '../tests/runner/setup_utils'; import { initializeFarmsForReserve } from '../tests/runner/farms/farms_operations'; import { Scope } from '@kamino-finance/scope-sdk'; const STAGING_LENDING_MARKET = new PublicKey('6WVSwDQXrBZeQVnu6hpnsRZhodaJTZBUaC334SiiBKdb'); const MAINNET_LENDING_MARKET = new PublicKey('7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF'); async function main() { const commands = new Command(); commands.name('klend-cli').description('CLI to interact with the klend program'); commands .command('print-borrow-rate') .option(`--url <string>`, 'The admin keypair file') .option(`--token <string>`, 'The token symbol') .option(`--cluster <string>`, 'staging or mainnet-beta') .action(async ({ url, token, cluster }) => { const connection = new Connection(url, {}); const programId = getProgramId(cluster); const kaminoMarket = await getMarket(connection, programId); const reserve = kaminoMarket.getReserveBySymbol(token); const slot = await connection.getSlot(); const borrowApr = reserve!.calculateBorrowAPR(slot, kaminoMarket.state.referralFeeBps); const utilizationRatio = reserve!.calculateUtilizationRatio(); console.log( `Reserve: ${parseTokenSymbol( reserve!.state.config.tokenInfo.name )} Borrow Rate: ${borrowApr} Utilization Ratio: ${utilizationRatio}` ); }); commands .command('print-all-lending-market-accounts') .option(`--rpc <string>`, 'The RPC URL') .action(async ({ rpc }) => { const connection = new Connection(rpc, {}); let count = 0; for await (const [address, lendingMarketAccount] of getAllLendingMarketAccounts(connection)) { count++; console.log( address.toString(), lendingMarketAccount.riskCouncil.toString(), lendingMarketAccount.autodeleverageEnabled, lendingMarketAccount.individualAutodeleverageMarginCallPeriodSecs.toString() ); } console.log(`Total lending markets: ${count}`); }); commands .command('print-all-markets-lite') .option(`--rpc <string>`, 'The RPC URL') .action(async ({ rpc }) => { const startTime = Date.now(); const connection = new Connection(rpc, {}); const slotDuration = await getMedianSlotDurationInMsFromLastEpochs(); const kaminoManager = new KaminoManager(connection, slotDuration); const allMarkets = await kaminoManager.getAllMarkets(); for (const market of allMarkets) { console.log( `Market: ${market.getName()} Address: ${ market.address } Deposit TVL: ${market.getTotalDepositTVL()} Borrow TVL: ${market.getTotalBorrowTVL()} TVL: ${market .getTotalDepositTVL() .minus(market.getTotalBorrowTVL())}` ); } const duration = Date.now() - startTime; console.log(`Execution duration: ${duration}ms`); }); commands .command('print-obligation') .option(`--rpc <string>`, 'The rpc url') .option(`--cluster <string>`, 'staging or mainnet-beta') .option(`--obligation <string>`, 'The obligation id') .action(async ({ rpc, cluster, obligation }) => { const connection = new Connection(rpc, {}); const kaminoMarket = await getMarket(connection, cluster); const kaminoObligation = await KaminoObligation.load(kaminoMarket, new PublicKey(obligation)); console.log(toJson(kaminoObligation?.refreshedStats)); }); commands .command('print-all-obligation-accounts') .option(`--rpc <string>`, 'The RPC URL') .action(async ({ rpc }) => { const connection = new Connection(rpc, {}); let count = 0; for await (const [address, obligationAccount] of getAllObligationAccounts(connection)) { count++; if ( obligationAccount.autodeleverageTargetLtvPct > 0 || obligationAccount.autodeleverageMarginCallStartedTimestamp.toNumber() > 0 ) { console.log(address.toString(), toJson(obligationAccount.toJSON())); } } console.log(`Total obligations: ${count}`); }); commands .command('print-reserve') .option(`--url <string>`, 'The admin keypair file') .option(`--reserve <string>`, 'Reserve address') .option(`--symbol <string>`, 'Symbol (optional)') .action(async ({ url, reserve, symbol }) => { const connection = new Connection(url, {}); await printReserve(connection, reserve, symbol); }); commands .command('print-all-reserve-accounts') .option(`--rpc <string>`, 'The RPC URL') .action(async ({ rpc }) => { const connection = new Connection(rpc, {}); let count = 0; const logItems: { address: string; value: string; index: number }[] = []; for await (const [address, reserveAccount] of getAllReserveAccounts(connection)) { count++; const logItem = { address: address.toString(), value: reserveAccount.config.autodeleverageEnabled.toString(), index: count, }; logItems.push(logItem); } console.log(`Total reserves: ${count}`); console.log(logItems); }); commands .command('deposit') .option(`--url <string>`, 'Custom RPC URL') .option(`--owner <string>`, 'Custom RPC URL') .option(`--token <string>`, 'Custom RPC URL') .option(`--amount <string>`, 'Custom RPC URL') .action(async ({ url, owner, token, amount }) => { const wallet = parseKeypairFile(owner); const connection = new Connection(url, {}); const depositAmount = new BN(amount); await deposit(connection, wallet, token, depositAmount); }); commands .command('withdraw') .option(`--url <string>`, 'Custom RPC URL') .option(`--owner <string>`, 'Custom RPC URL') .option(`--token <string>`, 'Custom RPC URL') .option(`--amount <string>`, 'Custom RPC URL') .action(async ({ url, owner, token, amount }) => { const wallet = parseKeypairFile(owner); const connection = new Connection(url, {}); const depositAmount = new BN(amount); await withdraw(connection, wallet, token, depositAmount); }); commands .command('borrow') .option(`--url <string>`, 'Custom RPC URL') .option(`--owner <string>`, 'Custom RPC URL') .option(`--token <string>`, 'Custom RPC URL') .option(`--amount <string>`, 'Custom RPC URL') .action(async ({ url, owner, token, amount }) => { const wallet = parseKeypairFile(owner); const connection = new Connection(url, {}); const borrowAmount = new BN(amount); await borrow(connection, wallet, token, borrowAmount); }); commands .command('repay') .option(`--url <string>`, 'Custom RPC URL') .option(`--owner <string>`, 'Custom RPC URL') .option(`--token <string>`, 'Custom RPC URL') .option(`--amount <string>`, 'Custom RPC URL') .action(async ({ url, owner, token, amount }) => { const wallet = parseKeypairFile(owner); const connection = new Connection(url, {}); const borrowAmount = new BN(amount); await repay(connection, wallet, token, borrowAmount); }); commands .command('init-farms-for-reserve') .option(`--cluster <string>`, 'Custom RPC URL') .option(`--owner <string>`, 'Owner keypair file') .option(`--reserve <string>`, 'Reserve pubkey') .option(`--farms-global-config <string>`, 'Reserve pubkey') .option(`--kind <string>`, '`Debt` or `Collateral`') .option(`--multisig`, 'Wether to use multisig or not -> prints bs58 txn') .option(`--simulate`, 'Wether to simulate the transaction or not') .action(async ({ cluster, owner, reserve, farmsGlobalConfig, kind, multisig, simulate }) => { const admin = parseKeypairFile(owner); const env = await initEnv(cluster, admin); await initFarmsForReserveCommand(env, reserve, kind, farmsGlobalConfig, multisig, simulate); }); commands .command('download-user-metadatas-without-lut') .option(`--cluster <string>`, 'Custom RPC URL') .option(`--program <string>`, 'Program pubkey') .option(`--output <string>`, 'Output file path - will print to stdout if not provided') .action(async ({ cluster, program, output }) => { const env = await initEnv(cluster); await downloadUserMetadatasWithFilter( env, [ { memcmp: { offset: 48, bytes: PublicKey.default.toBase58(), }, }, ], output, new PublicKey(program) ); }); commands .command('download-user-metadatas-without-owner') .option(`--cluster <string>`, 'Custom RPC URL') .option(`--program <string>`, 'Program pubkey') .option(`--output <string>`, 'Output file path - will print to stdout if not provided') .action(async ({ cluster, program, output }) => { const env = await initEnv(cluster); await downloadUserMetadatasWithFilter( env, [ { memcmp: { offset: 80, bytes: PublicKey.default.toBase58(), }, }, ], output, new PublicKey(program) ); }); commands .command('download-user-metadatas-without-owner-and-lut') .option(`--cluster <string>`, 'Custom RPC URL') .option(`--program <string>`, 'Program pubkey') .option(`--output <string>`, 'Output file path - will print to stdout if not provided') .action(async ({ cluster, program, output }) => { const env = await initEnv(cluster); await downloadUserMetadatasWithFilter( env, [ { memcmp: { offset: 80, bytes: PublicKey.default.toBase58(), }, }, { memcmp: { offset: 48, bytes: PublicKey.default.toBase58(), }, }, ], output, new PublicKey(program) ); }); commands .command('get-user-obligation-for-reserve') .option(`--cluster <string>`, 'Custom RPC URL') .option(`--program <string>`, 'Program pubkey') .option(`--lending-market <string>`, 'Lending market to fetch for') .option(`--user <string>`, 'User address to fetch for') .option(`--reserve <string>`, 'Reserve to fetch for') .action(async ({ cluster, program, lendingMarket, user, reserve }) => { const env = await initEnv(cluster); const programId = new PublicKey(program); const marketAddress = new PublicKey(lendingMarket); const kaminoMarket = await KaminoMarket.load( env.provider.connection, marketAddress, DEFAULT_RECENT_SLOT_DURATION_MS, programId ); const obligations = await kaminoMarket!.getAllUserObligationsForReserve( new PublicKey(user), new PublicKey(reserve) ); for (const obligation of obligations) { console.log('obligation address: ', obligation.obligationAddress.toString()); } }); commands .command('get-user-vanilla-obligation-for-reserve') .option(`--cluster <string>`, 'Custom RPC URL') .option(`--program <string>`, 'Program pubkey') .option(`--lending-market <string>`, 'Lending market to fetch for') .option(`--user <string>`, 'User address to fetch for') .action(async ({ cluster, program, lendingMarket, user }) => { const env = await initEnv(cluster); const programId = new PublicKey(program); const marketAddress = new PublicKey(lendingMarket); const kaminoMarket = await KaminoMarket.load( env.provider.connection, marketAddress, DEFAULT_RECENT_SLOT_DURATION_MS, programId ); const obligation = await kaminoMarket!.getUserVanillaObligation(new PublicKey(user)); console.log('obligation address: ', obligation ? obligation?.obligationAddress.toString() : 'null'); }); await commands.parseAsync(); } async function deposit(connection: Connection, wallet: Keypair, token: string, depositAmount: BN) { const programId = getProgramId('staging'); const kaminoMarket = await getMarket(connection, programId); const kaminoAction = await KaminoAction.buildDepositTxns( kaminoMarket, depositAmount, kaminoMarket.getReserveBySymbol(token)!.getLiquidityMint(), wallet.publicKey, new VanillaObligation(STAGING_LENDING_MARKET), true, { scope: new Scope('mainnet-beta', connection), scopeFeed: 'hubble' } ); console.log('User obligation', kaminoAction.getObligationPda().toString()); const tx = await buildVersionedTransaction(connection, wallet.publicKey, KaminoAction.actionToIxs(kaminoAction)); console.log('Deposit SetupIxs:', kaminoAction.setupIxsLabels); console.log('Deposit LendingIxs:', kaminoAction.lendingIxsLabels); console.log('Deposit CleanupIxs:', kaminoAction.cleanupIxsLabels); await buildAndSendTxnWithLogs(connection, tx, wallet, []); } async function withdraw(connection: Connection, wallet: Keypair, token: string, depositAmount: BN) { const programId = getProgramId('staging'); const kaminoMarket = await getMarket(connection, programId); const kaminoAction = await KaminoAction.buildWithdrawTxns( kaminoMarket, depositAmount, kaminoMarket.getReserveBySymbol(token)!.getLiquidityMint(), wallet.publicKey, new VanillaObligation(new PublicKey(STAGING_LENDING_MARKET)), true, { scope: new Scope('mainnet-beta', connection), scopeFeed: 'hubble' } ); console.log('User obligation', kaminoAction.getObligationPda().toString()); const tx = await buildVersionedTransaction(connection, wallet.publicKey, KaminoAction.actionToIxs(kaminoAction)); console.log('Withdraw SetupIxs:', kaminoAction.setupIxsLabels); console.log('Withdraw LendingIxs:', kaminoAction.lendingIxsLabels); console.log('Withdraw CleanupIxs:', kaminoAction.cleanupIxsLabels); await buildAndSendTxnWithLogs(connection, tx, wallet, []); } async function borrow(connection: Connection, wallet: Keypair, token: string, borrowAmount: BN) { const programId = getProgramId('staging'); const kaminoMarket = await getMarket(connection, programId); const kaminoAction = await KaminoAction.buildBorrowTxns( kaminoMarket, borrowAmount, kaminoMarket.getReserveBySymbol(token)!.getLiquidityMint(), wallet.publicKey, new VanillaObligation(new PublicKey(STAGING_LENDING_MARKET)), true, { scope: new Scope('mainnet-beta', connection), scopeFeed: 'hubble' } ); console.log('User obligation', kaminoAction.getObligationPda().toString()); const tx = await buildVersionedTransaction(connection, wallet.publicKey, KaminoAction.actionToIxs(kaminoAction)); console.log('Borrow SetupIxs:', kaminoAction.setupIxsLabels); console.log('Borrow LendingIxs:', kaminoAction.lendingIxsLabels); console.log('Borrow CleanupIxs:', kaminoAction.cleanupIxsLabels); await buildAndSendTxnWithLogs(connection, tx, wallet, []); } async function repay(connection: Connection, wallet: Keypair, token: string, borrowAmount: BN) { const programId = getProgramId('staging'); const kaminoMarket = await getMarket(connection, programId); const kaminoAction = await KaminoAction.buildRepayTxns( kaminoMarket, borrowAmount, kaminoMarket.getReserveBySymbol(token)!.getLiquidityMint(), wallet.publicKey, new VanillaObligation(new PublicKey(STAGING_LENDING_MARKET)), true, { scope: new Scope('mainnet-beta', connection), scopeFeed: 'hubble' }, await connection.getSlot() ); console.log('User obligation', kaminoAction.getObligationPda().toString()); const tx = await buildVersionedTransaction(connection, wallet.publicKey, KaminoAction.actionToIxs(kaminoAction)); console.log('Repay SetupIxs:', kaminoAction.setupIxsLabels); console.log('Repay LendingIxs:', kaminoAction.lendingIxsLabels); console.log('Repay CleanupIxs:', kaminoAction.cleanupIxsLabels); await buildAndSendTxnWithLogs(connection, tx, wallet, []); } async function printReserve(connection: Connection, reserve?: string, symbol?: string) { const programId = getProgramId('staging'); const kaminoMarket = await getMarket(connection, programId); const result = reserve ? kaminoMarket.getReserveByAddress(new PublicKey(reserve)) : kaminoMarket.getReserveBySymbol(symbol!); console.log(result); console.log(result?.stats?.reserveDepositLimit.toString()); } async function initFarmsForReserveCommand( env: Env, reserve: string, kind: string, farmsGlobalConfig: string, multisig: boolean, simulate: boolean, programId: PublicKey = PROGRAM_ID ) { const reserveState = await Reserve.fetch(env.connection, new PublicKey(reserve), programId); await initializeFarmsForReserve( env, reserveState!!.lendingMarket, new PublicKey(reserve), kind, multisig, simulate, farmsGlobalConfig ); } async function getMarket(connection: Connection, programId: PublicKey) { let marketAddress: PublicKey; if (programId.equals(STAGING_PROGRAM_ID)) { marketAddress = STAGING_LENDING_MARKET; } else if (programId.equals(PROGRAM_ID)) { marketAddress = MAINNET_LENDING_MARKET; } else { throw new Error(`Unknown program id: ${programId.toString()}`); } const kaminoMarket = await KaminoMarket.load(connection, marketAddress, DEFAULT_RECENT_SLOT_DURATION_MS, programId); if (kaminoMarket === null) { throw new Error(`${programId.toString()} Kamino market ${marketAddress.toBase58()} not found`); } return kaminoMarket; } async function downloadUserMetadatasWithFilter( env: Env, filter: GetProgramAccountsFilter[], output: string, programId: PublicKey ) { const userMetadatas = await getAllUserMetadatasWithFilter(env.connection, filter, programId); // help mapping const userPubkeys = userMetadatas.map((userMetadatas) => userMetadatas.address.toString()); if (output) { fs.writeFileSync(output, JSON.stringify(userPubkeys, null, 2)); } else { for (const userPubkey of userPubkeys) { console.log(userPubkey); } } console.log('Total of ' + userPubkeys.length + ' userMetadatas filtered'); } main() .then(() => { process.exit(); }) .catch((e) => { console.error('\n\nKamino CLI exited with error:\n\n', e); process.exit(1); }); export function parseKeypairFile(file: string): Keypair { return Keypair.fromSecretKey(Buffer.from(JSON.parse(require('fs').readFileSync(file)))); }