UNPKG

@raydium-io/raydium-sdk-v2

Version:

An SDK for building applications on top of Raydium.

343 lines (298 loc) 10.4 kB
import { Commitment, ComputeBudgetProgram, Connection, EpochInfo, Keypair, PublicKey, SimulatedTransactionResponse, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; import { createLogger } from "../logger"; import { CacheLTA } from "./lookupTable"; import { InstructionType } from "./txType"; import { ComputeBudgetConfig } from "../../raydium/type"; import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; const logger = createLogger("Raydium_txUtil"); export const MAX_BASE64_SIZE = 1644; export function addComputeBudget(config: ComputeBudgetConfig): { instructions: TransactionInstruction[]; instructionTypes: string[]; } { const ins: TransactionInstruction[] = []; const insTypes: string[] = []; if (config.microLamports) { ins.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: config.microLamports })); insTypes.push(InstructionType.SetComputeUnitPrice); } if (config.units) { ins.push(ComputeBudgetProgram.setComputeUnitLimit({ units: config.units })); insTypes.push(InstructionType.SetComputeUnitLimit); } return { instructions: ins, instructionTypes: insTypes, }; } export async function getRecentBlockHash(connection: Connection, propsCommitment?: Commitment): Promise<string> { const commitment = propsCommitment ?? "confirmed"; return (await connection.getLatestBlockhash?.({ commitment }))?.blockhash; } export async function confirmTransaction(connection: Connection, txId: string): Promise<string> { connection.getSignatureStatuses([txId]); return new Promise((resolve, reject) => { const id = setTimeout(reject, 60 * 1000); connection.onSignature( txId, (signatureResult) => { clearTimeout(id); if (!signatureResult.err) { resolve(""); return; } reject(Object.assign(signatureResult.err, { txId })); }, "confirmed", ); }); } /** * Forecast transaction size */ export function forecastTransactionSize(instructions: TransactionInstruction[], signers: PublicKey[]): boolean { if (instructions.length < 1) logger.logWithError(`no instructions provided: ${instructions.toString()}`); if (signers.length < 1) logger.logWithError(`no signers provided:, ${signers.toString()}`); const transaction = new Transaction(); transaction.recentBlockhash = "11111111111111111111111111111111"; transaction.feePayer = signers[0]; transaction.add(...instructions); try { return Buffer.from(transaction.serialize({ verifySignatures: false })).toString("base64").length < MAX_BASE64_SIZE; } catch (error) { return false; } } /** * Simulates multiple instruction */ /** * Simulates multiple instruction */ export async function simulateMultipleInstruction( connection: Connection, instructions: TransactionInstruction[], keyword: string, batchRequest = true, ): Promise<string[]> { const feePayer = new PublicKey("RaydiumSimuLateTransaction11111111111111111"); const transactions: Transaction[] = []; let transaction = new Transaction(); transaction.feePayer = feePayer; for (const instruction of instructions) { if (!forecastTransactionSize([...transaction.instructions, instruction], [feePayer])) { transactions.push(transaction); transaction = new Transaction(); transaction.feePayer = feePayer; } transaction.add(instruction); } if (transaction.instructions.length > 0) { transactions.push(transaction); } let results: SimulatedTransactionResponse[] = []; try { results = await simulateTransaction(connection, transactions, batchRequest); if (results.find((i) => i.err !== null)) throw Error("rpc simulateTransaction error"); } catch (error) { if (error instanceof Error) { logger.logWithError("failed to simulate for instructions", "RPC_ERROR", { message: error.message, }); } } const logs: string[] = []; for (const result of results) { logger.debug("simulate result:", result); if (result.logs) { const filteredLog = result.logs.filter((log) => log && log.includes(keyword)); logger.debug("filteredLog:", logs); if (!filteredLog.length) logger.logWithError("simulate log not match keyword", "keyword", keyword); logs.push(...filteredLog); } } return logs; } export function parseSimulateLogToJson(log: string, keyword: string): any { const results = log.match(/{["\w:,]+}/g); if (!results || results.length !== 1) { return logger.logWithError(`simulate log fail to match json, keyword: ${keyword}`); } return results[0]; } export function parseSimulateValue(log: string, key: string): any { const reg = new RegExp(`"${key}":(\\d+)`, "g"); const results = reg.exec(log); if (!results || results.length !== 2) { return logger.logWithError(`simulate log fail to match key", key: ${key}`); } return results[1]; } export interface ProgramAddress { publicKey: PublicKey; nonce: number; } export function findProgramAddress( seeds: Array<Buffer | Uint8Array>, programId: PublicKey, ): { publicKey: PublicKey; nonce: number; } { const [publicKey, nonce] = PublicKey.findProgramAddressSync(seeds, programId); return { publicKey, nonce }; } export async function simulateTransaction( connection: Connection, transactions: Transaction[], batchRequest?: boolean, ): Promise<any[]> { let results: any[] = []; if (batchRequest) { const getLatestBlockhash = await connection.getLatestBlockhash(); const encodedTransactions: string[] = []; for (const transaction of transactions) { transaction.recentBlockhash = getLatestBlockhash.blockhash; transaction.lastValidBlockHeight = getLatestBlockhash.lastValidBlockHeight; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const message = transaction._compile(); const signData = message.serialize(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const wireTransaction = transaction._serialize(signData); const encodedTransaction = wireTransaction.toString("base64"); encodedTransactions.push(encodedTransaction); } const batch = encodedTransactions.map((keys) => { const args = connection._buildArgs([keys], undefined, "base64"); return { methodName: "simulateTransaction", args, }; }); const reqData: { methodName: string; args: any[] }[][] = []; const itemReqIndex = 20; for (let i = 0; i < Math.ceil(batch.length / itemReqIndex); i++) { reqData.push(batch.slice(i * itemReqIndex, (i + 1) * itemReqIndex)); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore results = await ( await Promise.all( reqData.map(async (i) => (await (connection as any)._rpcBatchRequest(i)).map((ii) => ii.result.value)), ) ).flat(); } else { try { results = await Promise.all( transactions.map(async (transaction) => await (await connection.simulateTransaction(transaction)).value), ); } catch (error) { if (error instanceof Error) { logger.logWithError("failed to get info for multiple accounts", "RPC_ERROR", { message: error.message, }); } } } return results; } export function checkLegacyTxSize({ instructions, payer, signers, }: { instructions: TransactionInstruction[]; payer: PublicKey; signers: PublicKey[]; }): boolean { return forecastTransactionSize(instructions, [payer, ...signers]); } export function checkV0TxSize({ instructions, payer, lookupTableAddressAccount, recentBlockhash = Keypair.generate().publicKey.toString(), }: { instructions: TransactionInstruction[]; payer: PublicKey; lookupTableAddressAccount?: CacheLTA; recentBlockhash?: string; }): boolean { const transactionMessage = new TransactionMessage({ payerKey: payer, recentBlockhash, instructions, }); const messageV0 = transactionMessage.compileToV0Message(Object.values(lookupTableAddressAccount ?? {})); try { const buildLength = Buffer.from(new VersionedTransaction(messageV0).serialize()).toString("base64").length; return buildLength < MAX_BASE64_SIZE; } catch (error) { return false; } } let epochInfoCache: { time: number; data?: EpochInfo } = { time: 0, data: undefined, }; export async function getEpochInfo(connection: Connection): Promise<EpochInfo> { if (!epochInfoCache.data || (Date.now() - epochInfoCache.time) / 1000 > 30) { const data = await connection.getEpochInfo(); epochInfoCache = { time: Date.now(), data, }; return data; } else { return epochInfoCache.data; } } export const toBuffer = (arr: Buffer | Uint8Array | Array<number>): Buffer => { if (Buffer.isBuffer(arr)) { return arr; } else if (arr instanceof Uint8Array) { return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength); } else { return Buffer.from(arr); } }; export const txToBase64 = (transaction: Transaction | VersionedTransaction): string => { let serialized = transaction.serialize({ requireAllSignatures: false, verifySignatures: false }); if (transaction instanceof VersionedTransaction) serialized = toBuffer(serialized); try { return serialized instanceof Buffer ? serialized.toString("base64") : Buffer.from(serialized).toString("base64"); } catch { return serialized.toString("base64"); } }; export function printSimulate(transactions: Transaction[] | VersionedTransaction[]): string[] { const allBase64: string[] = []; transactions.forEach((transaction) => { if (transaction instanceof Transaction) { if (!transaction.recentBlockhash) transaction.recentBlockhash = TOKEN_PROGRAM_ID.toBase58(); if (!transaction.feePayer) transaction.feePayer = Keypair.generate().publicKey; } allBase64.push(txToBase64(transaction)); }); console.log("simulate tx string:", allBase64); return allBase64; } export function transformTxToBase64(tx: Transaction | VersionedTransaction): string { let serialized = tx.serialize({ requireAllSignatures: false, verifySignatures: false }); if (tx instanceof VersionedTransaction) serialized = toBuffer(serialized); return serialized.toString("base64"); }