UNPKG

@kamino-finance/klend-sdk

Version:

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

357 lines (330 loc) 10.2 kB
import { AccountInfoBase, AccountInfoWithJsonData, AccountInfoWithPubkey, Address, Base58EncodedBytes, fetchEncodedAccount, GetAccountInfoApi, GetMultipleAccountsApi, GetTokenAccountBalanceApi, Instruction, Lamports, MaybeAccount, Rpc, SolanaRpcApi, TransactionSigner, } from '@solana/kit'; import Decimal from 'decimal.js'; import { collToLamportsDecimal, DECIMALS_SOL } from '@kamino-finance/kliquidity-sdk/dist'; import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getSyncNativeInstruction, getCreateAssociatedTokenIdempotentInstruction, fetchMaybeToken, Token, getCloseAccountInstruction, } from '@solana-program/token-2022'; import { WRAPPED_SOL_MINT } from './pubkey'; import { getTransferSolInstruction } from '@solana-program/system'; /** * Create an idempotent create ATA instruction * Overrides the create ATA ix to use the idempotent version as the spl-token library does not provide this ix yet * @param owner - owner of the ATA * @param mint - mint of the ATA * @param payer - payer of the transaction * @param tokenProgram - optional token program address - spl-token if not provided * @param ata - optional ata address - derived if not provided * @returns The ATA address public key and the transaction instruction */ export async function createAssociatedTokenAccountIdempotentInstruction( payer: TransactionSigner, mint: Address, owner: Address = payer.address, tokenProgram: Address = TOKEN_PROGRAM_ADDRESS, ata?: Address ): Promise<[Address, Instruction]> { let ataAddress = ata; if (!ataAddress) { ataAddress = await getAssociatedTokenAddress(mint, owner, tokenProgram, ASSOCIATED_TOKEN_PROGRAM_ADDRESS); } const createUserTokenAccountIx = getCreateAssociatedTokenIdempotentInstruction( { owner, mint, tokenProgram, ata: ataAddress, payer, }, { programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS, } ); return [ataAddress, createUserTokenAccountIx]; } export async function getAssociatedTokenAddress( mint: Address, owner: Address, tokenProgram: Address = TOKEN_PROGRAM_ADDRESS, associatedTokenProgramId: Address = ASSOCIATED_TOKEN_PROGRAM_ADDRESS ): Promise<Address> { const [ata] = await findAssociatedTokenPda( { mint, owner, tokenProgram, }, { programAddress: associatedTokenProgramId } ); return ata; } export const getAtasWithCreateIxsIfMissing = async ( rpc: Rpc<GetMultipleAccountsApi>, user: TransactionSigner, mints: Array<{ mint: Address; tokenProgram: Address }> ): Promise<{ atas: Address[]; createAtaIxs: Instruction[] }> => { if (mints.length === 0) { return { atas: [], createAtaIxs: [] }; } const atas: Array<Address> = await Promise.all( mints.map(async (x) => getAssociatedTokenAddress(x.mint, user.address, x.tokenProgram)) ); const accountInfos = await rpc.getMultipleAccounts(atas).send(); const createAtaIxs: Instruction[] = []; for (let i = 0; i < atas.length; i++) { if (accountInfos.value[i] === null) { const { mint, tokenProgram } = mints[i]; const [ata, createIxn] = await createAssociatedTokenAccountIdempotentInstruction( user, mint, user.address, tokenProgram ); atas[i] = ata; createAtaIxs.push(createIxn); } } return { atas, createAtaIxs, }; }; export async function createAtasIdempotent( user: TransactionSigner, mints: Array<{ mint: Address; tokenProgram: Address }>, payer?: TransactionSigner ): Promise<Array<{ ata: Address; createAtaIx: Instruction }>> { const ataPayer = payer ?? user; const res: Array<{ ata: Address; createAtaIx: Instruction }> = []; for (const mint of mints) { const [ata, createAtaIx] = await createAssociatedTokenAccountIdempotentInstruction( ataPayer, mint.mint, user.address, mint.tokenProgram ); res.push({ ata, createAtaIx, }); } return res; } export function getTransferWsolIxs( owner: TransactionSigner, ata: Address, amountLamports: Lamports, tokenProgram: Address = TOKEN_PROGRAM_ADDRESS ) { const ixs: Instruction[] = []; ixs.push( getTransferSolInstruction({ source: owner, amount: amountLamports, destination: ata, }) ); ixs.push( getSyncNativeInstruction( { account: ata, }, { programAddress: tokenProgram } ) ); return ixs; } export async function getTokenAccountBalance( connection: Rpc<GetTokenAccountBalanceApi>, tokenAccount: Address ): Promise<number> { const tokenAccountBalance = await connection.getTokenAccountBalance(tokenAccount).send(); return Number(tokenAccountBalance.value.amount).valueOf(); } /// Get the balance of a token account in decimal format (tokens, not lamports) export async function getTokenAccountBalanceDecimal( rpc: Rpc<GetAccountInfoApi & GetTokenAccountBalanceApi>, mint: Address, owner: Address, tokenProgram: Address = TOKEN_PROGRAM_ADDRESS ): Promise<Decimal> { const ata = await getAssociatedTokenAddress(mint, owner, tokenProgram); const accInfo = await fetchEncodedAccount(rpc, ata); if (!accInfo.exists) { return new Decimal('0'); } const { value } = await rpc.getTokenAccountBalance(ata).send(); return new Decimal(value.uiAmountString!); } export type CreateWsolAtaIxs = { wsolAta: Address; createAtaIxs: Instruction[]; closeAtaIxs: Instruction[]; }; /** * Creates a wSOL ata if missing and syncs the balance. If the ata exists and it has more or equal no wrapping happens * @param rpc - Solana RPC rpc (read) * @param amount min amount to have in the wSOL ata. If the ata exists and it has more or equal no wrapping happens * @param owner - owner of the ata * @returns wsolAta: the keypair of the ata, used to sign the initialization transaction; createAtaIxs: a list with ixs to initialize the ata and wrap SOL if needed; closeAtaIxs: a list with ixs to close the ata */ export const createWsolAtaIfMissing = async ( rpc: Rpc<GetAccountInfoApi & GetTokenAccountBalanceApi>, amount: Decimal, owner: TransactionSigner, tokenProgram: Address = TOKEN_PROGRAM_ADDRESS ): Promise<CreateWsolAtaIxs> => { const createIxs: Instruction[] = []; const closeIxs: Instruction[] = []; const wsolAta: Address = await getAssociatedTokenAddress(WRAPPED_SOL_MINT, owner.address, tokenProgram); const solDeposit = amount; const wsolAtaAccountInfo: MaybeAccount<Token> = await fetchMaybeToken(rpc, wsolAta); // This checks if we need to create it if (!wsolAtaAccountInfo.exists) { createIxs.push( getCreateAssociatedTokenIdempotentInstruction({ owner: owner.address, payer: owner, ata: wsolAta, mint: WRAPPED_SOL_MINT, tokenProgram: tokenProgram, }) ); } let wsolExistingBalanceLamports = new Decimal(0); try { if (wsolAtaAccountInfo.exists) { const uiAmount = ( await getTokenAccountBalanceDecimal(rpc, WRAPPED_SOL_MINT, owner.address, tokenProgram) ).toNumber(); wsolExistingBalanceLamports = collToLamportsDecimal(new Decimal(uiAmount), DECIMALS_SOL); } } catch (err) { console.log('Err Token Balance', err); } if (solDeposit !== null && solDeposit.gt(wsolExistingBalanceLamports)) { createIxs.push( getTransferSolInstruction({ source: owner, destination: wsolAta, amount: BigInt(solDeposit.sub(wsolExistingBalanceLamports).floor().toString()), }) ); } if (createIxs.length > 0) { // Primitive way of wrapping SOL createIxs.push( getSyncNativeInstruction( { account: wsolAta, }, { programAddress: tokenProgram } ) ); } closeIxs.push( getCloseAccountInstruction( { owner, account: wsolAta, destination: owner.address, }, { programAddress: tokenProgram } ) ); return { wsolAta, createAtaIxs: createIxs, closeAtaIxs: closeIxs, }; }; /** * Get all standard token accounts for tokens using old Token Program, not Token 2022 for a given wallet * @param rpc - Solana RPC rpc (read) * @param wallet - wallet to get the token accounts for * @returns an array of all token accounts for the given wallet */ export async function getAllStandardTokenProgramTokenAccounts( rpc: Rpc<SolanaRpcApi>, wallet: Address ): Promise<AccountInfoWithPubkey<AccountInfoBase & AccountInfoWithJsonData>[]> { return rpc .getProgramAccounts(TOKEN_PROGRAM_ADDRESS, { filters: [ { dataSize: 165n }, { memcmp: { offset: 32n, bytes: wallet.toString() as Base58EncodedBytes, encoding: 'base58' } }, ], encoding: 'jsonParsed', }) .send(); } // Type definitions for parsed token account data interface ParsedTokenAccountInfo { mint: string; owner: string; tokenAmount: { amount: string; decimals: number; uiAmount: number | null; uiAmountString: string; }; } interface ParsedTokenAccountData { parsed: { info: ParsedTokenAccountInfo; type: string; }; program: string; space: bigint; } // Type guard to check if account data is parsed function isParsedTokenAccountData(data: any): data is ParsedTokenAccountData { return ( data && typeof data === 'object' && 'parsed' in data && data.parsed && typeof data.parsed === 'object' && 'info' in data.parsed && data.parsed.info && typeof data.parsed.info === 'object' && 'mint' in data.parsed.info && 'tokenAmount' in data.parsed.info ); } // Helper function to safely get mint from parsed token account export function getTokenAccountMint(accountData: any): string | null { if (isParsedTokenAccountData(accountData)) { return accountData.parsed.info.mint; } return null; } // Helper function to safely get token amount from parsed token account export function getTokenAccountAmount(accountData: any): number | null { if (isParsedTokenAccountData(accountData)) { return accountData.parsed.info.tokenAmount.uiAmount; } return null; }