@orca-so/tx-sender
Version:
Send transactions to the Solana blockchain with auto priority fees.
532 lines (524 loc) • 16.3 kB
JavaScript
// src/compatibility.ts
import { createSolanaRpcApi, createRpc } from "@solana/rpc";
import { createDefaultRpcTransport, address } from "@solana/kit";
function rpcFromUrl(url) {
const api = createSolanaRpcApi({
defaultCommitment: "confirmed"
});
const transport = createDefaultRpcTransport({ url });
const rpc = createRpc({ api, transport });
return rpc;
}
function normalizeAddresses(addresses) {
return addresses?.map((addr) => address(addr)) ?? [];
}
// src/config.ts
var globalConfig = {};
var DEFAULT_COMPUTE_UNIT_MARGIN_MULTIPLIER = 1.1;
var DEFAULT_PRIORITIZATION = {
priorityFee: {
type: "dynamic",
maxCapLamports: BigInt(4e6),
// 0.004 SOL
priorityFeePercentile: "50"
},
jito: {
type: "dynamic",
maxCapLamports: BigInt(4e6),
// 0.004 SOL
priorityFeePercentile: "50"
},
computeUnitMarginMultiplier: DEFAULT_COMPUTE_UNIT_MARGIN_MULTIPLIER,
jitoBlockEngineUrl: "https://bundles.jito.wtf"
};
var getRpcConfig = () => {
const rpcConfig = globalConfig.rpcConfig;
if (!rpcConfig?.rpcUrl) {
throw new Error("Connection not initialized. Call setRpc() first");
}
return rpcConfig;
};
var getPriorityConfig = () => {
if (!globalConfig.transactionConfig) {
return DEFAULT_PRIORITIZATION;
}
return globalConfig.transactionConfig;
};
var getJitoConfig = () => {
return getPriorityConfig().jito;
};
var getPriorityFeeConfig = () => {
return getPriorityConfig().priorityFee;
};
var getComputeUnitMarginMultiplier = () => {
return getPriorityConfig().computeUnitMarginMultiplier;
};
var getJitoBlockEngineUrl = () => {
return getPriorityConfig().jitoBlockEngineUrl;
};
var setGlobalConfig = (config) => {
globalConfig = {
transactionConfig: config.transactionConfig || DEFAULT_PRIORITIZATION,
rpcConfig: config.rpcConfig
};
};
async function setRpc(url, options = {}) {
const rpc = rpcFromUrl(url);
const chainId = await getChainIdFromGenesisHash(rpc);
setGlobalConfig({
...globalConfig,
rpcConfig: {
rpcUrl: url,
supportsPriorityFeePercentile: options.supportsPriorityFeePercentile ?? false,
chainId,
pollIntervalMs: options.pollIntervalMs ?? 0,
resendOnPoll: options.resendOnPoll ?? true
}
});
const nonThenableRpc = new Proxy(rpc, {
get(target, prop, receiver) {
if (prop === "then") {
return void 0;
}
return Reflect.get(target, prop, receiver);
}
});
return nonThenableRpc;
}
async function getChainIdFromGenesisHash(rpc) {
try {
const genesisHash = await rpc.getGenesisHash().send();
const genesisHashToChainId = {
"5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d": "solana",
EAQLJCV2mh23BsK2P9oYpV5CHVLDNHTxYss3URrNmg3s: "eclipse",
EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG: "solana-devnet",
CX4huckiV9QNAkKNVKi5Tj8nxzBive5kQimd94viMKsU: "eclipse-testnet"
};
return genesisHashToChainId[genesisHash] || "unknown";
} catch (error) {
console.warn("Error getting chain ID from genesis hash", error);
return "unknown";
}
}
async function setJitoBlockEngineUrl(url) {
setGlobalConfig({
...globalConfig,
transactionConfig: {
...getPriorityConfig(),
jitoBlockEngineUrl: url
}
});
}
function setPriorityFeeSetting(priorityFee) {
setGlobalConfig({
...globalConfig,
transactionConfig: {
...getPriorityConfig(),
priorityFee
}
});
}
function setJitoTipSetting(jito) {
setGlobalConfig({
...globalConfig,
transactionConfig: {
...getPriorityConfig(),
jito
}
});
}
function setComputeUnitMarginMultiplier(multiplier) {
setGlobalConfig({
...globalConfig,
transactionConfig: {
...getPriorityConfig(),
computeUnitMarginMultiplier: multiplier
}
});
}
function setJitoFeePercentile(percentile) {
const jito = getPriorityConfig().jito;
setGlobalConfig({
...globalConfig,
transactionConfig: {
...getPriorityConfig(),
jito: {
...jito,
priorityFeePercentile: percentile
}
}
});
}
function setPriorityFeePercentile(percentile) {
const priorityConfig = getPriorityConfig();
const priorityFee = priorityConfig.priorityFee;
setGlobalConfig({
...globalConfig,
transactionConfig: {
...priorityConfig,
priorityFee: {
...priorityFee,
priorityFeePercentile: percentile
}
}
});
}
// src/buildTransaction.ts
import {
compressTransactionMessageUsingAddressLookupTables,
assertAccountDecoded,
appendTransactionMessageInstructions,
createTransactionMessage,
pipe,
setTransactionMessageLifetimeUsingBlockhash,
partiallySignTransactionMessageWithSigners,
setTransactionMessageFeePayerSigner
} from "@solana/kit";
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
// src/priorityFees.ts
import { estimateComputeUnitLimitFactory } from "@solana-program/compute-budget";
// src/jito.ts
import { getTransferSolInstruction } from "@solana-program/system";
import {
address as address2,
lamports,
prependTransactionMessageInstruction
} from "@solana/kit";
async function processJitoTipForTxMessage(message, signer, jito, chainId) {
if (chainId !== "solana") {
console.warn("Jito tip is not supported on this chain. Skipping jito tip.");
return message;
}
let jitoTipLamports = BigInt(0);
if (jito.type === "exact") {
jitoTipLamports = jito.amountLamports;
} else if (jito.type === "dynamic") {
jitoTipLamports = await recentJitoTip(jito.priorityFeePercentile);
}
if (jitoTipLamports > 0) {
return prependTransactionMessageInstruction(
getTransferSolInstruction({
source: signer,
destination: getJitoTipAddress(),
amount: jitoTipLamports
}),
message
);
} else {
return message;
}
}
async function recentJitoTip(priorityFeePercentile) {
const blockEngineUrl = getJitoBlockEngineUrl();
const response = await fetch(`${blockEngineUrl}/api/v1/bundles/tip_floor`);
if (!response.ok) {
return BigInt(0);
}
const data = await response.json().then((res) => res[0]);
const percentileToKey = {
"25": "landed_tips_25th_percentile",
"50": "landed_tips_50th_percentile",
"75": "landed_tips_75th_percentile",
"95": "landed_tips_95th_percentile",
"99": "landed_tips_99th_percentile",
"50ema": "ema_landed_tips_50th_percentile"
};
const key = percentileToKey[priorityFeePercentile ?? "50"];
if (!key || !data[key]) {
return BigInt(0);
}
return lamports(BigInt(Math.floor(Number(data[key]) * 10 ** 9))).valueOf();
}
var jitoTipAddresses = [
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"
];
function getJitoTipAddress() {
return address2(
jitoTipAddresses[Math.floor(Math.random() * jitoTipAddresses.length)]
);
}
// src/computeBudget.ts
import {
getSetComputeUnitPriceInstruction,
getSetComputeUnitLimitInstruction
} from "@solana-program/compute-budget";
import {
prependTransactionMessageInstruction as prependTransactionMessageInstruction2,
isWritableRole
} from "@solana/kit";
async function processComputeBudgetForTxMessage(message, computeUnits) {
const { rpcUrl, supportsPriorityFeePercentile } = getRpcConfig();
const priorityFee = getPriorityFeeConfig();
const computeUnitMarginMultiplier = getComputeUnitMarginMultiplier();
let priorityFeeMicroLamports = BigInt(0);
if (priorityFee.type === "exact") {
priorityFeeMicroLamports = priorityFee.amountLamports * BigInt(1e6) / BigInt(computeUnits);
} else if (priorityFee.type === "dynamic") {
const estimatedPriorityFee = await calculateDynamicPriorityFees(
message.instructions,
rpcUrl,
supportsPriorityFeePercentile,
priorityFee.priorityFeePercentile ?? "50"
);
if (!priorityFee.maxCapLamports) {
priorityFeeMicroLamports = estimatedPriorityFee;
} else {
const maxCapMicroLamports = priorityFee.maxCapLamports * BigInt(1e6) / BigInt(computeUnits);
priorityFeeMicroLamports = maxCapMicroLamports > estimatedPriorityFee ? estimatedPriorityFee : maxCapMicroLamports;
}
}
if (priorityFeeMicroLamports > 0) {
message = prependTransactionMessageInstruction2(
getSetComputeUnitPriceInstruction({
microLamports: priorityFeeMicroLamports
}),
message
);
}
message = prependTransactionMessageInstruction2(
getSetComputeUnitLimitInstruction({
units: Math.ceil(
computeUnits * (computeUnitMarginMultiplier ?? DEFAULT_COMPUTE_UNIT_MARGIN_MULTIPLIER)
)
}),
message
);
return message;
}
function getWritableAccounts(ixs) {
const writable = /* @__PURE__ */ new Set();
ixs.forEach((ix) => {
if (ix.accounts) {
ix.accounts.forEach((acc) => {
if (isWritableRole(acc.role)) writable.add(acc.address);
});
}
});
return Array.from(writable);
}
async function calculateDynamicPriorityFees(instructions, rpcUrl, supportsPercentile, percentile) {
const writableAccounts = getWritableAccounts(instructions);
if (supportsPercentile) {
return await getRecentPrioritizationFeesWithPercentile(
rpcUrl,
writableAccounts,
percentile
);
} else {
const rpc = rpcFromUrl(rpcUrl);
const recent = await rpc.getRecentPrioritizationFees(writableAccounts).send();
const nonZero = recent.filter((pf) => pf.prioritizationFee > 0).map((pf) => pf.prioritizationFee);
const sorted = nonZero.sort((a, b) => Number(a - b));
return sorted[Math.floor(sorted.length * (parseInt(percentile) / 100))] || BigInt(0);
}
}
async function getRecentPrioritizationFeesWithPercentile(rpcEndpoint, writableAccounts, percentile) {
const response = await fetch(rpcEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "getRecentPrioritizationFees",
params: [
{
lockedWritableAccounts: writableAccounts,
percentile: parseInt(percentile) * 100
}
]
})
});
const data = await response.json();
if (data.error) {
throw new Error(`RPC error: ${data.error.message}`);
}
const last150Slots = data.result;
last150Slots.sort((a, b) => Number(a.slot - b.slot));
const last50Slots = last150Slots.slice(-50);
const nonZeroFees = last50Slots.filter((slot) => slot.prioritizationFee > 0);
if (nonZeroFees.length === 0) return BigInt(0);
const sorted = nonZeroFees.map((slot) => slot.prioritizationFee).sort((a, b) => Number(a - b));
const medianIndex = Math.floor(sorted.length / 2);
return sorted[medianIndex];
}
// src/priorityFees.ts
async function addPriorityInstructions(message, signer) {
const { rpcUrl, chainId } = getRpcConfig();
const jito = getJitoConfig();
const rpc = rpcFromUrl(rpcUrl);
if (jito.type !== "none") {
message = await processJitoTipForTxMessage(message, signer, jito, chainId);
}
let computeUnits = await getComputeUnitsForTxMessage(rpc, message);
return processComputeBudgetForTxMessage(message, computeUnits);
}
async function getComputeUnitsForTxMessage(rpc, txMessage) {
const estimator = estimateComputeUnitLimitFactory({
rpc
});
try {
const estimate = await estimator(txMessage);
return estimate;
} catch {
console.warn(
"Transaction simulation failed, using 1,400,000 compute units"
);
return 14e5;
}
}
// src/buildTransaction.ts
async function buildTransaction(instructions, feePayer, lookupTableAddresses) {
return buildTransactionMessage(
instructions,
feePayer,
normalizeAddresses(lookupTableAddresses)
);
}
async function buildTransactionMessage(instructions, feePayer, lookupTableAddresses) {
const { rpcUrl } = getRpcConfig();
const rpc = rpcFromUrl(rpcUrl);
let message = await prepareTransactionMessage(instructions, rpc, feePayer);
if (lookupTableAddresses?.length) {
const lookupTableAccounts = await fetchAllMaybeAddressLookupTable(
rpc,
lookupTableAddresses
);
const tables = lookupTableAccounts.reduce(
(prev, account) => {
if (account.exists) {
assertAccountDecoded(account);
prev[account.address] = account.data.addresses;
}
return prev;
},
{}
);
message = compressTransactionMessageUsingAddressLookupTables(
message,
tables
);
}
const messageWithPriorityFees = await addPriorityInstructions(
message,
feePayer
);
return await partiallySignTransactionMessageWithSigners(
messageWithPriorityFees
);
}
async function prepareTransactionMessage(instructions, rpc, feePayer) {
const { value: blockhash } = await rpc.getLatestBlockhash({
commitment: "confirmed"
}).send();
return pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageLifetimeUsingBlockhash(blockhash, tx),
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx)
);
}
// src/sendTransaction.ts
import {
assertIsFullySignedTransaction,
getBase64EncodedWireTransaction,
getBase58Decoder
} from "@solana/kit";
async function buildAndSendTransaction(instructions, payer, lookupTableAddresses, commitment = "confirmed") {
const tx = await buildTransaction(instructions, payer, lookupTableAddresses);
assertIsFullySignedTransaction(tx);
return sendTransaction(tx, commitment);
}
async function sendTransaction(transaction, commitment = "confirmed") {
assertIsFullySignedTransaction(transaction);
const { rpcUrl, pollIntervalMs, resendOnPoll } = getRpcConfig();
const rpc = rpcFromUrl(rpcUrl);
const txHash = getTxHash(transaction);
const encodedTransaction = getBase64EncodedWireTransaction(transaction);
const simResult = await rpc.simulateTransaction(encodedTransaction, {
encoding: "base64"
}).send();
if (simResult.value.err) {
throw new Error(
`Transaction simulation failed: ${JSON.stringify(
simResult.value.err,
(key, value) => typeof value === "bigint" ? value.toString() : value
)}`
);
}
const sendTx = async () => {
await rpc.sendTransaction(encodedTransaction, {
skipPreflight: true,
encoding: "base64",
...resendOnPoll && { maxRetries: BigInt(0) }
}).send();
};
try {
await sendTx();
} catch (error) {
throw new Error(`Failed to send transaction: ${error}`);
}
const expiryTime = Date.now() + 9e4;
while (Date.now() < expiryTime) {
const iterationStart = Date.now();
try {
const { value } = await rpc.getSignatureStatuses([txHash]).send();
const status = value[0];
if (status?.confirmationStatus === commitment) {
if (status.err) {
throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`);
}
return txHash;
}
} catch {
}
if (resendOnPoll) {
try {
await sendTx();
} catch {
}
}
if (pollIntervalMs > 0) {
const elapsed = Date.now() - iterationStart;
const remainingDelay = pollIntervalMs - elapsed;
if (remainingDelay > 0) {
await new Promise((resolve) => setTimeout(resolve, remainingDelay));
}
}
}
throw new Error("Transaction confirmation timeout");
}
function getTxHash(transaction) {
const [signature] = Object.values(transaction.signatures);
const txHash = getBase58Decoder().decode(signature);
return txHash;
}
export {
DEFAULT_COMPUTE_UNIT_MARGIN_MULTIPLIER,
DEFAULT_PRIORITIZATION,
buildAndSendTransaction,
buildTransaction,
getComputeUnitMarginMultiplier,
getJitoBlockEngineUrl,
getJitoConfig,
getPriorityFeeConfig,
getRpcConfig,
rpcFromUrl,
sendTransaction,
setComputeUnitMarginMultiplier,
setJitoBlockEngineUrl,
setJitoFeePercentile,
setJitoTipSetting,
setPriorityFeePercentile,
setPriorityFeeSetting,
setRpc
};
//# sourceMappingURL=index.js.map