@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
383 lines (350 loc) • 16 kB
text/typescript
import dotenv from 'dotenv';
import { Command } from 'commander';
import {
DEFAULT_RECENT_SLOT_DURATION_MS,
KaminoMarket,
KaminoObligation,
toJson,
getAllObligationAccounts,
getAllLendingMarketAccounts,
KaminoManager,
DEFAULT_PUBLIC_KEY,
} from '../lib';
import { address, Base58EncodedBytes } from '@solana/kit';
import BN from 'bn.js';
import { getMedianSlotDurationInMsFromLastEpochs, parseTokenSymbol } from '../classes/utils';
import { initEnv, parseEnv, SendTxMode } from './tx/CliEnv';
import { getMarket } from './services/market';
import { withdraw } from './commands/withdraw';
import { repay } from './commands/repay';
import { deposit } from './commands/deposit';
import { borrow } from './commands/borrow';
import { downloadUserMetadatasWithFilter } from './services/userMetadata';
import { initFarmsForReserve } from './commands/initFarmsForReserve';
import { printReserve } from './commands/printReserve';
import { printAllReserveAccounts } from './commands/printAllReserveAccounts';
dotenv.config({
path: `.env${process.env.ENV ? '.' + process.env.ENV : ''}`,
});
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(`--rpc <string>`, 'Custom RPC URL')
.requiredOption(`--token <string>`, 'The token symbol')
.requiredOption(`--market <string>`, 'The lending market address')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, token, market, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
const kaminoMarket = await getMarket(env.c.rpc, address(market), env.klendProgramId);
const reserve = kaminoMarket.getReserveBySymbol(token);
const slot = await env.c.rpc.getSlot().send();
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>`, 'Custom RPC URL')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
let count = 0;
for await (const [address, lendingMarketAccount] of getAllLendingMarketAccounts(env.c.rpc, env.klendProgramId)) {
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>`, 'Custom RPC URL')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, env: envFlag }) => {
const startTime = Date.now();
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
const slotDuration = await getMedianSlotDurationInMsFromLastEpochs();
const kaminoManager = new KaminoManager(
env.c.rpc,
slotDuration,
env.klendProgramId,
undefined,
undefined,
env.farmsProgramId
);
const allMarkets = await kaminoManager.getAllMarkets(env.klendProgramId);
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>`, 'Custom RPC URL')
.requiredOption(`--market <string>`, 'The lending market address')
.requiredOption(`--obligation <string>`, 'The obligation id')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, market, obligation, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
const kaminoMarket = await getMarket(env.c.rpc, address(market), env.klendProgramId);
const kaminoObligation = await KaminoObligation.load(kaminoMarket, address(obligation));
console.log(toJson(kaminoObligation?.refreshedStats));
});
commands
.command('print-all-obligation-accounts')
.option(`--rpc <string>`, 'Custom RPC URL')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
let count = 0;
for await (const [address, obligationAccount] of getAllObligationAccounts(env.c.rpc, env.klendProgramId)) {
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(`--rpc <string>`, 'Custom RPC URL')
.requiredOption(`--market <string>`, 'The lending market address')
.option(`--reserve <string>`, 'Reserve address')
.option(`--symbol <string>`, 'Symbol (optional)')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, market, reserve, symbol, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
await printReserve(env.c.rpc, address(market), env.klendProgramId, reserve, symbol);
});
commands
.command('print-all-reserve-accounts')
.option(`--rpc <string>`, 'Custom RPC URL')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
await printAllReserveAccounts(env.c.rpc, env.klendProgramId);
});
commands
.command('deposit')
.option(`--rpc <string>`, 'Custom RPC URL')
.requiredOption(`--owner <string>`, 'Owner keypair file')
.requiredOption(`--token <string>`, 'Token symbol')
.requiredOption(`--amount <string>`, 'Amount to deposit')
.requiredOption(`--market <string>`, 'The lending market address')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, owner, token, amount, market, env: envFlag }) => {
const depositAmount = new BN(amount);
const env = await initEnv(parseEnv(envFlag), owner, false, undefined, rpc);
await deposit(env, 'execute', token, depositAmount, address(market));
});
commands
.command('withdraw')
.option(`--rpc <string>`, 'Custom RPC URL')
.requiredOption(`--owner <string>`, 'Owner keypair file')
.requiredOption(`--token <string>`, 'Token symbol')
.requiredOption(`--amount <string>`, 'Amount to withdraw')
.requiredOption(`--market <string>`, 'The lending market address')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, owner, token, amount, market, env: envFlag }) => {
const withdrawAmount = new BN(amount);
const env = await initEnv(parseEnv(envFlag), owner, false, undefined, rpc);
await withdraw(env, 'execute', token, withdrawAmount, address(market));
});
commands
.command('borrow')
.option(`--rpc <string>`, 'Custom RPC URL')
.requiredOption(`--owner <string>`, 'Owner keypair file')
.requiredOption(`--token <string>`, 'Token symbol')
.requiredOption(`--amount <string>`, 'Amount to borrow')
.requiredOption(`--market <string>`, 'The lending market address')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, owner, token, amount, market, env: envFlag }) => {
const borrowAmount = new BN(amount);
const env = await initEnv(parseEnv(envFlag), owner, false, undefined, rpc);
await borrow(env, 'execute', token, borrowAmount, address(market));
});
commands
.command('repay')
.option(`--rpc <string>`, 'Custom RPC URL')
.requiredOption(`--owner <string>`, 'Owner keypair file')
.requiredOption(`--token <string>`, 'Token symbol')
.requiredOption(`--amount <string>`, 'Amount to repay')
.requiredOption(`--market <string>`, 'The lending market address')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, owner, token, amount, market, env: envFlag }) => {
const repayAmount = new BN(amount);
const env = await initEnv(parseEnv(envFlag), owner, false, undefined, rpc);
await repay(env, 'execute', token, repayAmount, address(market));
});
commands
.command('init-farms-for-reserve')
.option(`--rpc <string>`, 'Custom RPC URL')
.requiredOption(`--owner <string>`, 'Owner keypair file')
.requiredOption(`--reserve <string>`, 'Reserve pubkey')
.option(`--farms-global-config <string>`, 'Farms global config pubkey (defaults based on --env)')
.requiredOption(`--kind <string>`, '`Debt` or `Collateral`')
.option(`--multisig`, 'Whether to use multisig or not -> prints bs58 txn')
.option(`--simulate`, 'Whether to simulate the transaction or not')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, owner, reserve, farmsGlobalConfig, kind, multisig, simulate, env: envFlag }) => {
const env = await initEnv(
parseEnv(envFlag),
owner,
multisig,
{
farmsGlobalConfig: farmsGlobalConfig ? address(farmsGlobalConfig) : undefined,
},
rpc
);
const mode: SendTxMode = simulate ? 'simulate' : multisig ? 'multisig' : 'execute';
await initFarmsForReserve(env, mode, address(reserve), kind);
});
commands
.command('download-user-metadatas-without-lut')
.option(`--rpc <string>`, 'Custom RPC URL')
.option(`--program <string>`, 'Program pubkey')
.option(`--output <string>`, 'Output file path - will print to stdout if not provided')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, program, output, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
await downloadUserMetadatasWithFilter(
env.c,
[
{
memcmp: {
offset: 48n,
bytes: DEFAULT_PUBLIC_KEY.toString() as Base58EncodedBytes,
encoding: 'base58',
},
},
],
output,
program ? address(program) : env.klendProgramId
);
});
commands
.command('download-user-metadatas-without-owner')
.option(`--rpc <string>`, 'Custom RPC URL')
.option(`--program <string>`, 'Program pubkey')
.option(`--output <string>`, 'Output file path - will print to stdout if not provided')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, program, output, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
await downloadUserMetadatasWithFilter(
env.c,
[
{
memcmp: {
offset: 80n,
bytes: DEFAULT_PUBLIC_KEY.toString() as Base58EncodedBytes,
encoding: 'base58',
},
},
],
output,
program ? address(program) : env.klendProgramId
);
});
commands
.command('download-user-metadatas-without-owner-and-lut')
.option(`--rpc <string>`, 'Custom RPC URL')
.option(`--program <string>`, 'Program pubkey')
.option(`--output <string>`, 'Output file path - will print to stdout if not provided')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, program, output, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
await downloadUserMetadatasWithFilter(
env.c,
[
{
memcmp: {
offset: 80n,
bytes: DEFAULT_PUBLIC_KEY.toString() as Base58EncodedBytes,
encoding: 'base58',
},
},
{
memcmp: {
offset: 48n,
bytes: DEFAULT_PUBLIC_KEY.toString() as Base58EncodedBytes,
encoding: 'base58',
},
},
],
output,
program ? address(program) : env.klendProgramId
);
});
commands
.command('get-user-obligation-for-reserve')
.option(`--rpc <string>`, 'Custom RPC URL')
.option(`--program <string>`, 'Program pubkey (overrides --env)')
.requiredOption(`--lending-market <string>`, 'Lending market to fetch for')
.requiredOption(`--user <string>`, 'User address to fetch for')
.requiredOption(`--reserve <string>`, 'Reserve to fetch for')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, program, lendingMarket, user, reserve, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
const programId = program ? address(program) : env.klendProgramId;
const marketAddress = address(lendingMarket);
const kaminoMarket = await KaminoMarket.load(
env.c.rpc,
marketAddress,
DEFAULT_RECENT_SLOT_DURATION_MS,
programId
);
const obligations = await kaminoMarket!.getAllUserObligationsForReserve(address(user), address(reserve));
for (const obligation of obligations) {
console.log('obligation address: ', obligation.obligationAddress.toString());
}
});
commands
.command('get-user-vanilla-obligation-for-reserve')
.option(`--rpc <string>`, 'Custom RPC URL')
.option(`--program <string>`, 'Program pubkey (overrides --env)')
.requiredOption(`--lending-market <string>`, 'Lending market to fetch for')
.requiredOption(`--user <string>`, 'User address to fetch for')
.option(`--env <string>`, 'Environment: mainnet-beta (default), staging, devnet')
.action(async ({ rpc, program, lendingMarket, user, env: envFlag }) => {
const env = await initEnv(parseEnv(envFlag), undefined, false, undefined, rpc);
const programId = program ? address(program) : env.klendProgramId;
const marketAddress = address(lendingMarket);
const kaminoMarket = await KaminoMarket.load(
env.c.rpc,
marketAddress,
DEFAULT_RECENT_SLOT_DURATION_MS,
programId
);
const obligation = await kaminoMarket!.getUserVanillaObligation(address(user));
console.log('obligation address: ', obligation ? obligation?.obligationAddress.toString() : 'null');
});
await commands.parseAsync();
}
main()
.then(() => {
process.exit();
})
.catch((e) => {
console.error('\n\nKamino CLI exited with error:\n\n', e);
process.exit(1);
});