Last active
November 11, 2024 08:09
-
-
Save crypt0miester/8142b7f8c9cbaecc961f5438caaa2ddb to your computer and use it in GitHub Desktop.
Dynamic Compute Units and priority fees calculation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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