UNPKG

@orca-so/tx-sender

Version:

Send transactions to the Solana blockchain with auto priority fees.

532 lines (524 loc) 16.3 kB
// 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