import { TransactionError as Web3TxError } from '@solana/web3.js'; import Decimal from 'decimal.js'; import { buildTransactionMessage } from './utils'; import { COMPUTE_BUDGET_PROGRAM_ADDRESS, ComputeBudgetInstruction, getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, identifyComputeBudgetInstruction, } from '@solana-program/compute-budget'; import { Account, Address, AddressesByLookupTableAddress, Blockhash, compileTransactionMessage, FullySignedTransaction, getCompiledTransactionMessageEncoder, GetEpochInfoApi, GetSignatureStatusesApi, GetTransactionApi, Instruction, Rpc, RpcSubscriptions, sendAndConfirmTransactionFactory, SendTransactionApi, signature, Signature, SignatureNotificationsApi, SlotNotificationsApi, SolanaRpcApi, } from '@solana/kit'; import { AddressLookupTable, fetchAllAddressLookupTable } from '@solana-program/address-lookup-table'; const INVALID_BUT_SUFFICIENT_FOR_COMPILATION_BLOCKHASH: { blockhash: Blockhash; lastValidBlockHeight: bigint; slot: bigint; } = { blockhash: '11111111111111111111111111111111' as Blockhash, lastValidBlockHeight: 0n, slot: 0n, }; export function base64EncodeTx( cluster: 'mainnet-beta' | 'devnet' | 'localnet', payer: Address, instructions: Instruction[], lookupTables: Account[] | undefined = undefined, ): { simulationUrl: string; } { const luts: AddressesByLookupTableAddress = {}; (lookupTables || []).forEach((lut) => { luts[lut.address] = lut.data.addresses; }); const transactionMessage = buildTransactionMessage( instructions, payer, INVALID_BUT_SUFFICIENT_FOR_COMPILATION_BLOCKHASH, lookupTables, ); const compiled = compileTransactionMessage(transactionMessage); const encodedMessageBytes = getCompiledTransactionMessageEncoder().encode(compiled); const encodedTxMessage = Buffer.from(encodedMessageBytes).toString('base64'); const clusterString = cluster === 'localnet' ? '?cluster=custom&customUrl=http://localhost:8899' : `?cluster=${cluster.toString()}`; const simulationUrl = `https://explorer.solana.com/tx/inspector${clusterString}&message=${encodeURIComponent( encodedTxMessage, )}&signatures=${encodeURIComponent(`[${payer}]`)}`; return { simulationUrl }; } export function removeComputeBudgetIxs(ixs: Instruction[]): Instruction[] { return ixs.filter(({ programAddress }) => programAddress !== COMPUTE_BUDGET_PROGRAM_ADDRESS); } export async function sendAndConfirmTx( { rpc, wsRpc, }: { rpc: Rpc; wsRpc: RpcSubscriptions; }, tx: FullySignedTransaction, slot: bigint, ): Promise { await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: wsRpc })( { ...tx, lifetimeConstraint: { lastValidBlockHeight: slot } }, { commitment: 'confirmed', preflightCommitment: 'confirmed', maxRetries: 0n, skipPreflight: true, minContextSlot: slot, }, ); } export async function sendAndConfirmTransactionV0Impl( rpc: { rpc: Rpc; wsRpc: RpcSubscriptions; }, sig: Signature, tx: FullySignedTransaction, slot: bigint, withDescription: string = '', logger: (str: string) => void = console.log, ): Promise { try { const link = `https://solscan.io/tx/${sig}`; logger(`${withDescription} ${link}`); await sendAndConfirmTx(rpc, tx, slot); } catch (e) { const failedTx = await forceGetConfirmedTx(rpc.rpc, sig); if (!failedTx) { throw new TransactionError(`Failed to send transaction: ${withDescription} ${sig}`, sig, undefined, e); } else { let logs: string[] | undefined = undefined; if (failedTx.meta?.logMessages) { logs = [...failedTx.meta.logMessages]; } throw new TransactionError(`Failed to send transaction: ${withDescription} ${sig}`, sig, logs, e); } } } export async function confirmTx( connection: Rpc, txHash: string, ): Promise> { const sig = signature(txHash); return connection .getTransaction(sig, { commitment: 'confirmed', encoding: 'json', maxSupportedTransactionVersion: 0, }) .send(); } export type TransactionResponse = ReturnType | null; export async function forceGetConfirmedTx( rpc: Rpc, sig: Signature, ): Promise { console.log(`forceGetConfirmedTx: ${sig}`); const endTime = Date.now() + 5000; let lastErr = null; let failedTx: TransactionResponse | null = null; while (true) { try { failedTx = await rpc .getTransaction(sig, { commitment: 'confirmed', maxSupportedTransactionVersion: 0, encoding: 'json', }) .send(); if (failedTx) { return failedTx; } } catch (e) { lastErr = e; } if (Date.now() > endTime) { if (lastErr) { throw lastErr; } else { return failedTx; } } } } /** * @deprecated Use `fetchAllAddressLookupTable` from `@solana-program/address-lookup-table` */ export const getLookupTableAccountsFromKeys = async ( connection: Rpc, keys: Address[], ): Promise[]> => { return await fetchAllAddressLookupTable(connection, keys); }; export class TransactionError extends Error { sig: string; logs: string[] | undefined; err: Web3TxError | undefined; constructor(message: string, sig: string, logs?: string[], err?: Web3TxError) { super(message); this.sig = sig; this.logs = logs; this.err = err; } } export function createAddExtraComputeUnitsTransaction(units: number, feePerCULamports?: Decimal): Instruction[] { const ixns = []; ixns.push(getSetComputeUnitLimitInstruction({ units })); if (feePerCULamports) { const { microLamports } = getComputeUnitPrice(units, feePerCULamports); ixns.push(getSetComputeUnitPriceInstruction({ microLamports })); } return ixns; } export function getComputeUnitPrice( units: number, feePerCULamports: Decimal, ): { microLamports: bigint; } { const multiplier = 1; const unitPrice = feePerCULamports .mul(10 ** 6) .div(units) .mul(multiplier); const value = unitPrice.floor().toString(); console.debug(`Fee per CU lamports: ${feePerCULamports}, units: ${units}, compute unit price: ${value}`); return { microLamports: BigInt(value) }; } function isComputeBudgetComputeUnitLimit(ix: ComputeBudgetInstruction): boolean { return ix === ComputeBudgetInstruction.SetComputeUnitLimit; } export function overwriteComputeBudget(ixs: Instruction[], units: number): Instruction[] { const ixns = ixs.filter( (ix) => ix.programAddress !== COMPUTE_BUDGET_PROGRAM_ADDRESS || (ix.data && !isComputeBudgetComputeUnitLimit(identifyComputeBudgetInstruction(ix.data))), ); ixns.unshift(getSetComputeUnitLimitInstruction({ units })); return ixns; }