@kamino-finance/klend-sdk
Version:
Typescript SDK for interacting with the Kamino Lending (klend) protocol
281 lines (251 loc) • 8.54 kB
text/typescript
import Decimal from 'decimal.js';
import {
AddressLookupTableAccount,
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 = ''
): 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(5000);
const res = await c.getTransaction(sig, {
commitment: 'confirmed',
maxSupportedTransactionVersion: 6,
});
console.log('Transaction Logs:\n', res?.meta?.logMessages);
}
return sig;
} catch (e: any) {
console.log(e);
process.stdout.write(e.logs.toString());
await sleep(5000);
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,
ixns: TransactionInstruction[],
signers: Signer[],
lutAddresses: PublicKey[] = [],
description: string = ''
): Promise<TransactionSignature> {
const tx = await buildVersionedTransaction(c, owner.publicKey, ixns, 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());
await sleep(5000);
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 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 getComputeBudgetAndPriorityFeeIxns = (
units: number,
priorityFeeLamports?: Decimal
): TransactionInstruction[] => {
const ixns: TransactionInstruction[] = [];
ixns.push(ComputeBudgetProgram.setComputeUnitLimit({ units }));
if (priorityFeeLamports && priorityFeeLamports.gt(0)) {
const unitPrice = priorityFeeLamports.mul(10 ** 6).div(units);
ixns.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: BigInt(unitPrice.floor().toString()) }));
}
return ixns;
};
// 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 uniqueAccounts(
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((ixn) => {
ixn.keys.forEach((key) => {
uniqueAccounts.add(key.pubkey);
});
});
return uniqueAccounts.toArray();
}