@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
324 lines (288 loc) • 9.87 kB
text/typescript
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;
});
}