Skip to content

Instantly share code, notes, and snippets.

@crypt0miester
Last active November 11, 2024 08:09
Show Gist options
  • Save crypt0miester/8142b7f8c9cbaecc961f5438caaa2ddb to your computer and use it in GitHub Desktop.
Save crypt0miester/8142b7f8c9cbaecc961f5438caaa2ddb to your computer and use it in GitHub Desktop.
Dynamic Compute Units and priority fees calculation.
import {
AddressLookupTableAccount,
Blockhash,
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
Transaction,
TransactionError,
TransactionInstruction,
TransactionMessage,
VersionedTransaction,
type GetRecentPrioritizationFeesConfig,
type RecentPrioritizationFees,
} from "@solana/web3.js";
const rpcUrl = process.env.NEXT_PUBLIC_SOLANA_RPC_URL || "";
// easy to use values for user convenience
export const enum PriotitizationFeeLevels {
LOW = 2500,
MEDIAN = 5000,
HIGH = 7500,
MAX = 10000,
}
// extending the original interface to include the percentile and fallback options and maintain compatibility
export interface GetRecentPrioritizationFeesByPercentileConfig
extends GetRecentPrioritizationFeesConfig {
percentile?: PriotitizationFeeLevels | number;
fallback?: boolean;
}
interface RpcResponse {
jsonrpc: String;
id?: String;
result?: [];
error?: any;
}
export const getMinPrioritizationFeeByPercentile = async (
connection: Connection,
config: GetRecentPrioritizationFeesByPercentileConfig,
slotsToReturn?: number,
): Promise<number> => {
const recentPrioritizationFees =
await getRecentPrioritizationFeesByPercentile(
connection,
config,
slotsToReturn,
);
const minPriorityFee = recentPrioritizationFees.reduce((min, current) => {
return current.prioritizationFee < min.prioritizationFee ? current : min;
});
return minPriorityFee.prioritizationFee;
};
export const getMaxPrioritizationFeeByPercentile = async (
connection: Connection,
config: GetRecentPrioritizationFeesByPercentileConfig,
slotsToReturn?: number,
): Promise<number> => {
const recentPrioritizationFees =
await getRecentPrioritizationFeesByPercentile(
connection,
config,
slotsToReturn,
);
const maxPriorityFee = recentPrioritizationFees.reduce((max, current) => {
return current.prioritizationFee > max.prioritizationFee ? current : max;
});
return maxPriorityFee.prioritizationFee;
};
export const getMeanPrioritizationFeeByPercentile = async (
connection: Connection,
config: GetRecentPrioritizationFeesByPercentileConfig,
slotsToReturn?: number,
): Promise<number> => {
const recentPrioritizationFees =
await getRecentPrioritizationFeesByPercentile(
connection,
config,
slotsToReturn,
);
const mean = Math.ceil(
recentPrioritizationFees.reduce(
(acc, fee) => acc + fee.prioritizationFee,
0,
) / recentPrioritizationFees.length,
);
return mean;
};
export const getMedianPrioritizationFeeByPercentile = async (
connection: Connection,
config: GetRecentPrioritizationFeesByPercentileConfig,
slotsToReturn?: number,
): Promise<number> => {
const recentPrioritizationFees =
await getRecentPrioritizationFeesByPercentile(
connection,
config,
slotsToReturn,
);
recentPrioritizationFees.sort(
(a, b) => a.prioritizationFee - b.prioritizationFee,
);
const half = Math.floor(recentPrioritizationFees.length / 2);
if (recentPrioritizationFees.length % 2) {
return recentPrioritizationFees[half].prioritizationFee;
}
return Math.ceil(
(recentPrioritizationFees[half - 1].prioritizationFee +
recentPrioritizationFees[half].prioritizationFee) /
2,
);
};
// this function gets the recent prioritization fees from the RPC. The `rpcRequest` comes from webjs.Connection
const getRecentPrioritizationFeesFromRpc = async (
config: any,
rpcRequest: any,
) => {
const accounts = config?.lockedWritableAccounts?.map(
(key: { toBase58: () => any }) => key.toBase58(),
);
const args = accounts?.length ? [accounts] : [[]];
config.percentile && args.push({ percentile: config.percentile });
const response = await rpcRequest("getRecentPrioritizationFees", args);
return response;
};
export const getRecentPrioritizationFeesByPercentile = async (
connection: Connection,
config: GetRecentPrioritizationFeesByPercentileConfig,
slotsToReturn?: number,
): Promise<RecentPrioritizationFees[]> => {
const { fallback = true, lockedWritableAccounts = [] } = config || {};
slotsToReturn =
slotsToReturn && Number.isInteger(slotsToReturn) ? slotsToReturn : -1;
const promises = [];
let tritonRpcResponse: RpcResponse | undefined = undefined;
let fallbackRpcResponse: RpcResponse | undefined = undefined;
// @solana/web3.js uses the private method `_rpcRequest` internally to make RPC requests which is not exposed by TypeScript
// it is available in JavaScript, however, TypeScript enforces it as unavailable and complains, the following line is a workaround
/* @ts-ignore */
const rpcRequest = connection._rpcRequest;
// to save fallback roundtrips if your RPC is not Triton, both RPCs are called in parallel to minimize latency
promises.push(
getRecentPrioritizationFeesFromRpc(config, rpcRequest).then((result) => {
tritonRpcResponse = result;
}),
);
if (fallback) {
promises.push(
getRecentPrioritizationFeesFromRpc(
{ lockedWritableAccounts },
rpcRequest,
).then((result) => {
fallbackRpcResponse = result;
}),
);
}
await Promise.all(promises);
// satisfying typescript by casting the response to RpcResponse
const tritonGRPFResponse = tritonRpcResponse as unknown as RpcResponse;
const fallbackGRPFResponse = fallbackRpcResponse as unknown as RpcResponse;
let recentPrioritizationFees: RecentPrioritizationFees[] = [];
if (tritonGRPFResponse?.result) {
recentPrioritizationFees = tritonGRPFResponse.result!;
}
if (fallbackGRPFResponse?.result && !tritonGRPFResponse?.result) {
recentPrioritizationFees = fallbackGRPFResponse.result!;
}
if (fallback && fallbackGRPFResponse.error) {
return fallbackGRPFResponse.error;
}
if (tritonGRPFResponse?.error) {
return tritonGRPFResponse.error;
}
// sort the prioritization fees by slot
recentPrioritizationFees.sort((a, b) => a.slot - b.slot);
// return the first n prioritization fees
if (slotsToReturn > 0)
return recentPrioritizationFees.slice(0, slotsToReturn);
return recentPrioritizationFees;
};
interface RequestPayload {
method: string;
params: {
last_n_blocks: number;
account: string;
};
id: number;
jsonrpc: string;
}
interface FeeEstimates {
extreme: number;
high: number;
low: number;
medium: number;
percentiles: {
[key: string]: number;
};
}
interface ResponseData {
jsonrpc: string;
result: {
context: {
slot: number;
};
per_compute_unit: FeeEstimates;
per_transaction: FeeEstimates;
};
id: number;
}
export async function fetchEstimatePriorityFees(
accounts?: string[],
): Promise<number> {
// console.log(rpcUrl);
switch (true) {
// Helius does not work with the current implementation
// requires small changes
case rpcUrl.includes("helius"):
const heliusResponse = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getPriorityFeeEstimate",
params: [
{
accountKeys: accounts,
options: {
priorityLevel: "High"
},
}
],
}),
});
const heliusData = await heliusResponse.json();
const priorityFeeEstimate = heliusData.result?.priorityFeeEstimate;
console.log(
`priority fees: ${priorityFeeEstimate}`,
);
return parseInt(priorityFeeEstimate.toString());
case rpcUrl.includes("quicknode"):
const params: any = {};
const payload: RequestPayload = {
method: "qn_estimatePriorityFees",
params,
id: 1,
jsonrpc: "2.0",
};
const quicknodeResponse = await fetch(rpcUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!quicknodeResponse.ok) {
throw new Error(`HTTP error! status: ${quicknodeResponse.status}`);
}
const quicknodeData: ResponseData = await quicknodeResponse.json();
console.log(
`priority fees: ${quicknodeData.result.per_compute_unit.extreme}`,
);
return parseInt(quicknodeData.result.per_compute_unit.extreme.toString());
default:
const connection = new Connection(rpcUrl);
const config: GetRecentPrioritizationFeesByPercentileConfig = {
lockedWritableAccounts: accounts
? accounts.map((account) => new PublicKey(account))
: undefined,
percentile: PriotitizationFeeLevels.LOW,
};
const slotsToReturn: number | undefined = undefined; // Optional: specify if needed
const rpcpoolResult = await getRecentPrioritizationFeesByPercentile(
connection,
config,
slotsToReturn,
);
const fees = rpcpoolResult
.map((fee) => fee.prioritizationFee)
.filter((fee) => fee > 0);
const sortedFees = fees.sort((a, b) => b - a);
// console.log('sortedFees', sortedFees)
// THIRD HIGHEST FEES
// const thirdHighestFee = sortedFees.length > 1 ? sortedFees[2] : sortedFees[0];
// console.log(thirdHighestFee)
// const highestFee = Math.max(
// ...rpcpoolResult.map((fee) => fee.prioritizationFee),
// );
// console.log(highestFee)
// console.log(`priority fees ${highestFee}`);
// return thirdHighestFee;
// HIGHEST FEES
// Assuming you want to use the highest fee from the result
// const highestFee = Math.max(
// ...rpcpoolResult.map((fee) => fee.prioritizationFee),
// );
// // console.log(`priority fees ${highestFee}`);
// return highestFee;
// MEAN FEES
const fee = Math.ceil(
sortedFees.reduce((acc, fee) => acc + fee, 0) / sortedFees.length,
);
console.log(`priority fees: ${fee}`);
return fee;
}
}
export async function getSimulationUnitsTxn(
connection: Connection,
transaction: VersionedTransaction | Transaction,
signer?: Keypair,
computeErrorMargin: number = 500,
): Promise<{
units: number | undefined;
insufficientFunds: boolean;
slippageToleranceExceeded: boolean;
simulationError: TransactionError | string | null;
}> {
let simulation;
if (transaction instanceof VersionedTransaction) {
simulation = await connection.simulateTransaction(transaction, {
replaceRecentBlockhash: true,
sigVerify: false,
});
} else {
if (!signer) {
throw new Error(
"Failed to simulate. Either include signer or use VersionedTransaction",
);
}
simulation = await connection.simulateTransaction(transaction, [signer]);
}
// console.log(simulation)
let { insufficientFunds, slippageToleranceExceeded } = parseLogs(
simulation.value.logs,
);
let units: number | undefined;
if (simulation.value.err === "InsufficientFundsForFee") {
insufficientFunds = true;
}
if (simulation.value.unitsConsumed) {
units = parseInt(
(
simulation.value.unitsConsumed *
(1 + computeErrorMargin / 10_000)
).toString(),
);
} else {
units = simulation.value.err ? undefined : 200_000;
}
return {
units,
insufficientFunds,
slippageToleranceExceeded,
simulationError: simulation.value.err,
};
}
export function parseLogs(logMessages: Array<string> | null | undefined) {
let insufficientFunds = false;
let slippageToleranceExceeded = false;
if (!logMessages) {
return {
insufficientFunds,
slippageToleranceExceeded,
};
}
for (const message of logMessages) {
if (message.includes("Program log: Error: insufficient funds")) {
insufficientFunds = true;
} else if (
message.includes(
"Program log: AnchorError occurred. Error Code: SlippageToleranceExceeded. Error Number: 6001. Error Message: Slippage tolerance exceeded.",
)
) {
slippageToleranceExceeded = true;
} else if (message.includes("Transfer: insufficient lamports")) {
insufficientFunds = true;
}
}
return {
insufficientFunds,
slippageToleranceExceeded,
};
}
// Borrowed with love from Helium spl-utils package
// https://github.com/helium/helium-program-library/blob/68da8e38e769a22bca0492156695b9677978d139/packages/spl-utils/src/transaction.ts#L766
export async function getVersionedTransactionAndPriorityFees(
walletPubkey: PublicKey,
instructions: TransactionInstruction[],
blockhash: {
blockhash: Blockhash;
lastValidBlockHeight: number;
},
lookupTables?: AddressLookupTableAccount[],
): Promise<{ transaction: VersionedTransaction; priorityFees: number }> {
const priorityFees = await fetchEstimatePriorityFees(
instructions.flatMap((instruction) =>
instruction.keys.map((key) => key.pubkey.toString()),
),
);
const instructionsCompiled = [
ComputeBudgetProgram.setComputeUnitLimit({
units: 1000000,
}),
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFees,
}),
...instructions,
];
const transaction = new VersionedTransaction(
new TransactionMessage({
instructions: instructionsCompiled,
recentBlockhash: blockhash.blockhash,
payerKey: walletPubkey,
}).compileToV0Message(lookupTables),
);
return { transaction, priorityFees };
}
export async function getComputeUnitFeesWithPriorityFees(
connection: Connection,
instructions: TransactionInstruction[],
signerKey: PublicKey,
latestBlockHash: {
blockhash: Blockhash;
lastValidBlockHeight: number;
},
lookupTables?: AddressLookupTableAccount[],
): Promise<{
microLamportsEstimate: number;
computeUnits: number;
insufficientFunds: boolean;
slippageToleranceExceeded: boolean;
simulationError: TransactionError | string | null;
}> {
// Add all instructions into as many transactions as needed
const { transaction, priorityFees: microLamportsEstimate } =
await getVersionedTransactionAndPriorityFees(
signerKey,
instructions,
latestBlockHash,
lookupTables,
);
const {
units: computeUnits,
insufficientFunds,
slippageToleranceExceeded,
simulationError,
} = await getSimulationUnitsTxn(connection, transaction);
return {
microLamportsEstimate,
computeUnits: computeUnits || 350_000,
insufficientFunds,
slippageToleranceExceeded,
simulationError,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment