UNPKG

@kamino-finance/klend-sdk

Version:

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

324 lines (288 loc) 9.87 kB
import Decimal from 'decimal.js'; import { AddressLookupTableAccount, Blockhash, Commitment, ComputeBudgetProgram, Connection, Keypair, PublicKey, SendOptions, Signer, Transaction, TransactionInstruction, TransactionMessage, TransactionResponse, TransactionSignature, VersionedTransaction, VersionedTransactionResponse, } from '@solana/web3.js'; import { fromTxError } from '../idl_codegen/errors'; import { sleep } from '../classes/utils'; import { batchFetch } from '@kamino-finance/kliquidity-sdk'; import { PublicKeySet } from './pubkey'; export async function buildAndSendTxnWithLogs( c: Connection, tx: VersionedTransaction, owner: Keypair, signers: Signer[], withLogsIfSuccess: boolean = false, withDescription: string = '', throwOnError: boolean = false // TODO(error-surfacing): the "recovery" logic is broken/outdated + the "hide error" behavior seems wrong in general - migrate to "just throw" ): Promise<TransactionSignature> { tx.sign([owner, ...signers]); try { const sig: string = await sendAndConfirmVersionedTransaction(c, tx, 'confirmed', { preflightCommitment: 'processed', }); console.log('Transaction Hash:', withDescription, sig); if (withLogsIfSuccess) { await sleep(1000); const res = await c.getTransaction(sig, { commitment: 'confirmed', maxSupportedTransactionVersion: 6, }); console.log('Transaction Logs:\n', res?.meta?.logMessages); } return sig; } catch (e: any) { if (throwOnError) { throw e; } console.log(e); process.stdout.write(e.logs.toString()); const sig = e.toString().split(' failed ')[0].split('Transaction ')[1]; const res: VersionedTransactionResponse | null = await c.getTransaction(sig, { commitment: 'confirmed', maxSupportedTransactionVersion: 6, }); console.log('Txn', res!.meta!.logMessages); return sig; } } export async function buildAndSendTxn( c: Connection, owner: Keypair, ixs: TransactionInstruction[], signers: Signer[], lutAddresses: PublicKey[] = [], description: string = '' ): Promise<TransactionSignature> { const tx = await buildVersionedTransaction(c, owner.publicKey, ixs, lutAddresses); return await buildAndSendTxnWithLogs(c, tx, owner, signers, true, description); } export async function sendAndConfirmVersionedTransaction( c: Connection, tx: VersionedTransaction, commitment: Commitment = 'confirmed', sendTransactionOptions: SendOptions = { preflightCommitment: 'processed' } ) { const defaultOptions: SendOptions = { skipPreflight: true }; const txId = await c.sendTransaction(tx, { ...defaultOptions, ...sendTransactionOptions }); const latestBlockHash = await c.getLatestBlockhash('finalized'); const t = await c.confirmTransaction( { blockhash: latestBlockHash.blockhash, lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, signature: txId, }, commitment ); if (t.value && t.value.err) { const txDetails = await c.getTransaction(txId, { maxSupportedTransactionVersion: 0, commitment: 'confirmed', }); if (txDetails) { throw { err: txDetails.meta?.err, logs: txDetails.meta?.logMessages || [] }; } throw t.value.err; } return txId; } export async function simulateTxn(c: Connection, tx: Transaction, owner: Keypair, signers: Signer[]) { const { blockhash } = await c.getLatestBlockhash(); tx.recentBlockhash = blockhash; tx.feePayer = owner.publicKey; try { const simulation = await c.simulateTransaction(tx, [owner, ...signers]); console.log('Transaction Hash:', simulation); } catch (e: any) { console.log(e); process.stdout.write(e.logs.toString()); const sig = e.toString().split(' failed ')[0].split('Transaction ')[1]; const res: TransactionResponse | null = await c.getTransaction(sig, { commitment: 'confirmed', }); console.log('Txn', res!.meta!.logMessages); return sig; } } export function buildComputeBudgetIx(units: number): TransactionInstruction { return ComputeBudgetProgram.setComputeUnitLimit({ units }); } /** * Send a transaction with optional address lookup tables * Translates anchor errors into anchor error types * @param connection * @param payer * @param instructions * @param lookupTables */ export async function sendTransactionV0( connection: Connection, payer: Keypair, instructions: TransactionInstruction[], lookupTables: AddressLookupTableAccount[] | undefined = undefined, options?: SendOptions ): Promise<string> { const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; const messageV0 = new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash, instructions, }).compileToV0Message(lookupTables); const tx = new VersionedTransaction(messageV0); tx.sign([payer]); try { return await connection.sendTransaction(tx, options); } catch (err) { throw fromTxError(err) ?? err; } } export async function simulateTransactionV0( connection: Connection, payer: Keypair, instructions: TransactionInstruction[], lookupTables: AddressLookupTableAccount[] | undefined = undefined ) { const recentBlockhash = (await connection.getLatestBlockhash()).blockhash; const messageV0 = new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash, instructions, }).compileToV0Message(lookupTables); const tx = new VersionedTransaction(messageV0); tx.sign([payer]); try { return await connection.simulateTransaction(tx); } catch (err) { throw fromTxError(err) ?? err; } } export const buildVersionedTransaction = async ( connection: Connection, payer: PublicKey, instructions: TransactionInstruction[], lookupTables: PublicKey[] = [] ): Promise<VersionedTransaction> => { const blockhash = await connection.getLatestBlockhash('confirmed').then((res) => res.blockhash); const lookupTablesAccounts = await Promise.all( lookupTables.map((address) => { return getLookupTableAccount(connection, address); }) ); const messageV0 = new TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions, }).compileToV0Message(lookupTablesAccounts.filter(notEmpty)); return new VersionedTransaction(messageV0); }; export async function getLookupTableAccountsFromAddresses( connection: Connection, addresses: PublicKey[] ): Promise<AddressLookupTableAccount[]> { const lookupTablesAccounts = await Promise.all( addresses.map((address) => { return getLookupTableAccount(connection, address); }) ); return lookupTablesAccounts.filter((account) => account !== null) as AddressLookupTableAccount[]; } export const buildVersionedTransactionSync = ( payer: PublicKey, instructions: TransactionInstruction[], blockhash: Blockhash, lookupTables: AddressLookupTableAccount[] = [] ): VersionedTransaction => { const messageV0 = new TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions, }).compileToV0Message(lookupTables.filter(notEmpty)); return new VersionedTransaction(messageV0); }; export const getLookupTableAccount = async (connection: Connection, address: PublicKey) => { return connection.getAddressLookupTable(address).then((res) => res.value); }; export async function getLookupTableAccounts( connection: Connection, addresses: PublicKey[] ): Promise<AddressLookupTableAccount[]> { const lookupTableAccounts: AddressLookupTableAccount[] = []; const accountInfos = await batchFetch(addresses, (batch) => connection.getMultipleAccountsInfo(batch)); for (let i = 0; i < addresses.length; i++) { const info = accountInfos[i]; if (!info) { throw new Error(`Lookup table ${addresses[i]} is not found`); } lookupTableAccounts.push( new AddressLookupTableAccount({ key: addresses[i], state: AddressLookupTableAccount.deserialize(info.data), }) ); } return lookupTableAccounts; } export const getComputeBudgetAndPriorityFeeIxs = ( units: number, priorityFeeLamports?: Decimal ): TransactionInstruction[] => { const ixs: TransactionInstruction[] = []; ixs.push(ComputeBudgetProgram.setComputeUnitLimit({ units })); if (priorityFeeLamports && priorityFeeLamports.gt(0)) { const unitPrice = priorityFeeLamports.mul(10 ** 6).div(units); ixs.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: BigInt(unitPrice.floor().toString()) })); } return ixs; }; // filters null values from array and make typescript happy export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { if (value === null || value === undefined) { return false; } // // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars const testDummy: TValue = value; return true; } export function uniqueAccountsWithProgramIds( ixs: TransactionInstruction[], addressLookupTables: PublicKey[] | AddressLookupTableAccount[] = [] ): Array<PublicKey> { let luts: PublicKey[]; if (addressLookupTables.length > 0 && addressLookupTables[0] instanceof AddressLookupTableAccount) { luts = (addressLookupTables as AddressLookupTableAccount[]).map((lut) => lut.key); } else { luts = addressLookupTables as PublicKey[]; } const uniqueAccounts = new PublicKeySet<PublicKey>(luts); ixs.forEach((ix) => { uniqueAccounts.add(ix.programId); ix.keys.forEach((key) => { uniqueAccounts.add(key.pubkey); }); }); return uniqueAccounts.toArray(); } export function removeBudgetIxs(ixs: TransactionInstruction[]): TransactionInstruction[] { return ixs.filter((ix) => { const { programId } = ix; if (programId.equals(ComputeBudgetProgram.programId)) { return false; } return true; }); }