> ## Documentation Index
> Fetch the complete documentation index at: https://docs.range.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Transaction Simulation Guide

> 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.

<Info>
  **Prerequisites:** You need a [Risk API key](/introduction/getting-started)
  and familiarity with the [Transaction
  Simulator](/risk-api/simulator/simulate-solana-transaction) endpoint. You
  should also know how to construct Solana transactions using `@solana/kit` or
  equivalent.
</Info>

***

## When to Simulate

| Scenario                     | Why Simulate                                                                         |
| ---------------------------- | ------------------------------------------------------------------------------------ |
| **Pre-broadcast validation** | Confirm the transaction will succeed before committing it to the blockchain          |
| **Cost estimation**          | Get accurate compute units and fee breakdown before signing                          |
| **Debugging**                | Identify program errors, insufficient balances, or invalid instructions              |
| **Risk assessment**          | Check if any accounts involved in the transaction are malicious or suspicious        |
| **User confirmation**        | Show users exactly what will happen (balance changes, transfers) before they approve |

***

## Step 1: Prepare and Encode the Transaction

Build your Solana transaction using any SDK and encode it as base64 or base58.

<CodeGroup>
  ```typescript @solana/kit theme={null}
  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't
  require signatures, but Kit instruction builders // expect a TransactionSigner
  interface for accounts that will sign. function
  createNoopSigner<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) const
  compiled = compileTransaction(transactionMessage); const encoded =
  getBase64EncodedWireTransaction(compiled);

  ```

  ```javascript @solana/web3.js theme={null}
  import { Connection, Transaction, SystemProgram, PublicKey } from "@solana/web3.js";

  const SENDER_PUBKEY = new PublicKey("SenderAddress11111111111111111111111111111");
  const RECIPIENT_PUBKEY = new PublicKey("RecipientAddress111111111111111111111111");

  const connection = new Connection("https://api.mainnet-beta.solana.com");

  // Build the transaction
  const transaction = new Transaction().add(
    SystemProgram.transfer({
      fromPubkey: SENDER_PUBKEY,
      toPubkey: RECIPIENT_PUBKEY,
      lamports: 1_000_000_000, // 1 SOL
    })
  );

  // Set required fields
  transaction.recentBlockhash = (
    await connection.getLatestBlockhash()
  ).blockhash;
  transaction.feePayer = SENDER_PUBKEY;

  // Encode as base64 (do NOT sign yet - simulation doesn't require signatures)
  const encoded = transaction
    .serialize({ requireAllSignatures: false })
    .toString("base64");
  ```
</CodeGroup>

<Tip>
  You don't need to sign the transaction before simulating. The simulator
  accepts unsigned transactions, so you can validate before committing any
  signatures.
</Tip>

***

## Step 2: Submit for Simulation

### Single Transaction

```bash theme={null}
curl -X POST "https://api.range.org/v1/simulate/solana/transaction" \
  -H "Authorization: Bearer your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": "ENCODED_TRANSACTION_BASE64",
    "encoding_type": "base64"
  }'
```

```javascript theme={null}
async function simulateTransaction(encodedTx) {
  const response = await fetch(
    'https://api.range.org/v1/simulate/solana/transaction',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        payload: encodedTx,
        encoding_type: 'base64',
      }),
    },
  );
  return response.json();
}
```

### Batch Simulation

Use batch simulation when you have multiple transactions to validate - either
independent transactions or a sequence of dependent operations.

```javascript theme={null}
async function simulateBatch(transactions) {
  const response = await fetch(
    'https://api.range.org/v1/simulate/solana/transactions',
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        transactions: transactions.map(tx => ({
          payload: tx,
          encoding_type: 'base64',
        })),
      }),
    },
  );
  return response.json();
}
```

### When to Use Each

| Scenario                                        | Use               |
| ----------------------------------------------- | ----------------- |
| Single transfer or swap                         | Single simulation |
| Multi-step workflow (approve → swap → transfer) | Batch simulation  |
| Validating multiple independent transactions    | Batch simulation  |
| Portfolio rebalancing across multiple tokens    | Batch simulation  |

## Step 3: Analyze the Response

The simulation response contains several sections. Here's what to check and in
what order:

### Check for Errors First

If the `error` field is present, the transaction would fail onchain. Other
fields may be incomplete.

```javascript theme={null}
function checkSimulationSuccess(result) {
  if (result.error) {
    return {
      success: false,
      reason: result.error,
    };
  }
  return { success: true };
}
```

### Review Asset Transfers

The `asset_transfers` array shows all token and SOL movements per account. Each
entry represents a balance change for a specific account and asset.

```javascript theme={null}
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.

### Check Balance Changes

The `lamport_changes` array shows the net SOL balance change for each address
involved in the transaction.

```typescript theme={null}
function getBalanceChanges(result: SimulationResult) {
  return (result.lamport_changes || []).map(change => ({
    account: change.account,
    preBalance: change.pre_balance, // Lamports before
    postBalance: change.post_balance, // Lamports after
    delta: change.post_balance - change.pre_balance, // Net change in lamports
  }));
}
```

### Review State Changes

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.

```typescript theme={null}
function getStateChanges(result: SimulationResult) {
  const changes = result.expected_state_changes || {};
  return Object.entries(changes).flatMap(([account, accountChanges]) =>
    accountChanges.map(change => ({
      account,
      description: change.humanReadableDiff, // e.g., "Sent 3.317 SOL", "Receive 268.78 USDC"
      color: change.suggestedColor, // "CREDIT" or "DEBIT"
      kind: change.rawInfo?.kind, // "SOL_TRANSFER", "SPL_TRANSFER", etc.
      asset: change.rawInfo?.data?.asset, // Token metadata (symbol, name, decimals, imageUrl, price)
      diff: change.rawInfo?.data?.diff, // { sign: "PLUS" | "MINUS", digits: bigint }
      counterparty: change.rawInfo?.data?.counterparty, // For SPL transfers
    })),
  );
}
```

State change types include:

* `SOL_TRANSFER`, Native SOL transfers
* `SPL_TRANSFER`, Token transfers, swaps, mints, burns
* Account creations, closures, and other token operations

### Check Transaction Costs

```typescript theme={null}
function getCosts(result: SimulationResult) {
  const summary = result.transaction_summary;
  const fee = summary.expected_fee;
  return {
    computeUnits: summary.cus_consumed,
    totalFee: fee.total_fee, // Total fee in lamports
    computeFee: fee.compute_fee, // Base compute fee
    prioritizationFee: fee.prioritization_fee,
    jitoTip: fee.jito_tip, // Jito tip if applicable
    feeConfidence: fee.confidence, // "High", "Medium", or "Low"
    programsInvoked: summary.programs_invoked,
  };
}
```

#### Fee Confidence Levels

| Level      | Meaning                                                           | Action                                   |
| ---------- | ----------------------------------------------------------------- | ---------------------------------------- |
| **High**   | Derived from explicit compute budget instructions                 | Fee is accurate                          |
| **Medium** | Estimated from recent prioritization fees for the locked accounts | Fee is a reasonable estimate             |
| **Low**    | No data available; fallback logic used                            | Consider setting explicit compute budget |

***

## Step 4: Check Transaction Risk

The `transaction_risk` section provides risk analysis for all accounts involved
in the transaction.

```typescript theme={null}
function analyzeRisk(result: SimulationResult) {
  const risk = result.transaction_risk;

  // Check for exploit patterns (e.g., address poisoning)
  const exploits = risk.exploit_risks_detected || [];

  // Extract account risk data from the nested structure
  const accountsRisk = risk.accounts_risk_score?.data?.addresses_risk || [];

  // Find risky accounts (score >= 6)
  const riskyAccounts = accountsRisk.flatMap(entry =>
    entry.risk_scores
      .filter(score => score.riskScore >= 6)
      .map(score => ({
        address: score.address,
        score: score.riskScore,
        level: score.riskLevel,
        numHops: score.numHops,
        reasoning: score.reasoning,
        maliciousConnections: score.maliciousAddressesFound,
      })),
  );

  // Get summary stats
  const summary = risk.accounts_risk_score?.data?.summary;

  return {
    hasExploitRisk: exploits.length > 0,
    exploits,
    riskyAccounts,
    summary: summary
      ? {
          totalChecked: summary.total,
          criticalRisk: summary.critical_risk,
          highRisk: summary.high_risk,
          mediumRisk: summary.medium_risk,
          lowRisk: summary.low_risk,
          noRisk: summary.no_risk,
        }
      : null,
    isHighRisk: exploits.length > 0 || riskyAccounts.length > 0,
  };
}
```

Each risky account includes:

* `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.)

<Note>
  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.
</Note>

***

## Debugging Common Failures

| Error Pattern           | Likely Cause                            | Fix                                                 |
| ----------------------- | --------------------------------------- | --------------------------------------------------- |
| Insufficient funds      | Account doesn't have enough SOL/tokens  | Check balances before building the transaction      |
| Account not found       | Destination token account doesn't exist | Create the associated token account first           |
| Program error           | Invalid instruction data or state       | Check program-specific requirements                 |
| Blockhash expired       | Transaction took too long to simulate   | Use a fresh `recentBlockhash`                       |
| Compute budget exceeded | Transaction is too complex              | Add a compute budget instruction with higher limits |

### Using Logs for Debugging

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:

```typescript theme={null}
function findErrors(result: SimulationResult) {
  return (result.transaction_logs || []).filter(
    log =>
      log.includes('Error') ||
      log.includes('failed') ||
      log.includes('insufficient'),
  );
}
```

***

## Complete Simulation Workflow

```typescript theme={null}
async function validateBeforeSending(encodedTx: string) {
  const result = await simulateTransaction(encodedTx);

  // 1. Check if simulation succeeded
  if (result.error) {
    return {
      proceed: false,
      reason: `Transaction would fail: ${result.error}`,
    };
  }

  // 2. Check for exploit risks
  const risk = analyzeRisk(result);
  if (risk.hasExploitRisk) {
    return {
      proceed: false,
      reason: 'Exploit risk detected',
      details: risk.exploits,
    };
  }

  // 3. Check for risky accounts
  if (risk.riskyAccounts.length > 0) {
    return {
      proceed: false,
      reason: 'Transaction involves high-risk accounts',
      riskyAccounts: risk.riskyAccounts,
    };
  }

  // 4. Summarize for user confirmation
  return {
    proceed: true,
    transfers: summarizeTransfers(result),
    costs: getCosts(result),
    stateChanges: getStateChanges(result),
  };
}
```

***

## What's Next

<CardGroup cols={2}>
  <Card title="Simulate Solana Transaction" icon="play" href="/risk-api/simulator/simulate-solana-transaction">
    Full endpoint reference with response schema details.
  </Card>

  <Card title="Wallet Integration" icon="wallet" href="/risk-api/guides/wallet-dapp-integration">
    Integrate simulation into a wallet send flow with address screening.
  </Card>
</CardGroup>
