Validate Solana transactions before broadcasting using the Transaction Simulator for pre-flight checks, debugging, and risk assessment.
This guide covers how to use Range’s Transaction Simulator to validate Solana
transactions before they hit the blockchain. You’ll learn to interpret
simulation results, check for risks, debug failures, and decide between single
and batch simulation.
Prerequisites: You need a Risk API key
and familiarity with the Transaction
Simulator endpoint. You
should also know how to construct Solana transactions using @solana/kit or
equivalent.
Build your Solana transaction using any SDK and encode it as base64 or base58.
Copy
import { pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, compileTransaction, getBase64EncodedWireTransaction, createSolanaRpc, address, lamports, type TransactionSigner, type Address,} from "@solana/kit";import { getTransferSolInstruction } from "@solana-program/system";// No-op signer for building unsigned transactions. // Simulation doesn'trequire signatures, but Kit instruction builders // expect a TransactionSignerinterface for accounts that will sign. functioncreateNoopSigner<TAddress extends string>( pubkey: Address<TAddress> ):TransactionSigner<TAddress> { return { address: pubkey, signTransactions: async(txs) => txs, }; }const SENDER_ADDRESS = address("SenderAddress11111111111111111111111111111");const RECIPIENT_ADDRESS = address("RecipientAddress111111111111111111111111");const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com"); const {value: latestBlockhash } = await rpc.getLatestBlockhash().send();// Wrap sender in a no-op signer for the transfer instruction const senderSigner= createNoopSigner(SENDER_ADDRESS);// Build the transaction message const transactionMessage = pipe(createTransactionMessage({ version: 0 }), (m) =>setTransactionMessageFeePayer(SENDER_ADDRESS, m), (m) =>setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) =>appendTransactionMessageInstruction( getTransferSolInstruction({ source:senderSigner, destination: RECIPIENT_ADDRESS, amount: lamports(1_000_000_000n),// 1 SOL }), m ) );// Compile and encode as base64 (no signatures needed for simulation) constcompiled = compileTransaction(transactionMessage); const encoded =getBase64EncodedWireTransaction(compiled);
You don’t need to sign the transaction before simulating. The simulator
accepts unsigned transactions, so you can validate before committing any
signatures.
The asset_transfers array shows all token and SOL movements per account. Each
entry represents a balance change for a specific account and asset.
Copy
function summarizeTransfers(result) { return (result.asset_transfers || []).map(transfer => ({ account: transfer.account, // The account affected mint: transfer.mint, // Token mint address, or "SOL" for native SOL amount: transfer.amount, // Amount transferred (human-readable, not lamports) changeType: transfer.change_type, // "Debit" (outflow) or "Credit" (inflow) }));}
For a simple SOL transfer, you’ll see two entries: a Debit from the sender and
a Credit to the recipient. For swaps or complex transactions, you’ll see
multiple entries across different mints and accounts.
The expected_state_changes object maps each affected account to an array of
state changes. Each change includes a humanReadableDiff for display, a
suggestedColor (CREDIT or DEBIT), and rawInfo with structured data about
the change type.
riskScore (1-10) and riskLevel (e.g., “Extremely high risk”)
numHops — Distance to nearest malicious address
reasoning — Explanation of why the address is flagged
maliciousAddressesFound — List of connected malicious addresses with their
categories (Hack, Scam, etc.)
Risk analysis is performed on the main transaction level. Embedded
transactions (e.g., Squads multisig operations) and nested instruction data
are not currently included in risk scoring.
The transaction_logs array contains the complete execution logs from the
Solana runtime. Search for "Program log: Error" or "failed" to pinpoint the
failing instruction: