Transaction Simulator Before Submission Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Transaction Simulator Before Submission Development
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Developing a transaction simulator before submission

"Why did my gas disappear and the transaction still reverted?" — one of the most common questions from DeFi users. Most reverts are predictable: slippage exceeded, insufficient allowance, deadline passed. Simulating a transaction before submission solves this problem at its root and reduces failed transactions to single digits.

How simulation works

An Ethereum node allows calling eth_call or debug_traceCall — execute a transaction against the current (or historical) blockchain state without actually sending it. Get the result: success/revert + revert reason + state changes + gas usage.

Three levels of simulation depth:

eth_call — basic level. Returns return data or revert reason. Available on any node, fast. Doesn't show intermediate states.

debug_traceCall — full EVM trace: each OPCODE, storage reads/writes, internal calls. Requires a node with debug API (Alchemy, Tenderly, or self-hosted Erigon). Slow.

Tenderly Simulation API — most complete result out of the box: asset changes, state diff, event logs, gas breakdown. Paid, but significantly easier than custom implementation.

Implementation via eth_call

import { createPublicClient, http, encodeFunctionData, decodeFunctionResult } from "viem";
import { mainnet } from "viem/chains";

async function simulateTransaction(
  from: `0x${string}`,
  to: `0x${string}`,
  calldata: `0x${string}`,
  value: bigint = 0n
): Promise<SimulationResult> {
  const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
  
  try {
    const result = await client.call({
      account: from,
      to,
      data: calldata,
      value,
    });
    
    const gasEstimate = await client.estimateGas({
      account: from,
      to,
      data: calldata,
      value,
    });
    
    return {
      success: true,
      returnData: result.data,
      gasUsed: gasEstimate,
    };
  } catch (error) {
    // Parse revert reason
    const revertReason = parseRevertReason(error);
    return {
      success: false,
      revertReason,
      gasUsed: 0n,
    };
  }
}

function parseRevertReason(error: unknown): string {
  if (error instanceof ContractFunctionRevertedError) {
    return error.data?.errorName ?? error.shortMessage;
  }
  // Custom error decoding via ABI
  if (error instanceof Error && "data" in error) {
    return decodeCustomError(error.data as `0x${string}`);
  }
  return "Unknown revert";
}

Tenderly Simulation API

For production simulators with rich UX, Tenderly provides significantly more information:

async function simulateWithTenderly(params: {
  from: string;
  to: string;
  data: string;
  value?: string;
  gasLimit?: number;
}): Promise<TenderlySimulation> {
  const response = await fetch(
    `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT}/project/${TENDERLY_PROJECT}/simulate`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Access-Key": process.env.TENDERLY_API_KEY!,
      },
      body: JSON.stringify({
        network_id: "1",
        from: params.from,
        to: params.to,
        input: params.data,
        value: params.value ?? "0",
        gas: params.gasLimit ?? 3000000,
        gas_price: "0", // Gas price doesn't matter for simulation
        save: false,
      }),
    }
  );
  
  const sim = await response.json();
  
  return {
    success: sim.transaction.status,
    gasUsed: sim.transaction.gas_used,
    assetChanges: parseAssetChanges(sim.transaction.transaction_info),
    stateChanges: sim.transaction.transaction_info.state_diff,
    logs: sim.transaction.transaction_info.logs,
    revertReason: sim.transaction.error_message,
  };
}

Parsing asset changes for UX

Users need to see not raw state diff, but a clear summary:

interface AssetChange {
  type: "ERC20" | "ERC721" | "ETH";
  direction: "in" | "out";
  amount: string;
  symbol: string;
  tokenAddress?: string;
  tokenId?: string; // for ERC-721
}

function formatSimulationSummary(assetChanges: AssetChange[]): string[] {
  return assetChanges.map(change => {
    const arrow = change.direction === "in" ? "+" : "-";
    if (change.type === "ERC721") {
      return `${arrow} NFT #${change.tokenId} (${change.symbol})`;
    }
    return `${arrow} ${change.amount} ${change.symbol}`;
  });
}

// Result in UI:
// - 0.5 ETH
// + 1500 USDC
// - NFT #4521 (BAYC)

Integration into TransactionButton component

function SimulatedTransactionButton({ 
  contractAddress, 
  functionName, 
  args, 
  value,
  children 
}) {
  const { address } = useAccount();
  const [simulation, setSimulation] = useState<SimulationResult | null>(null);
  const [isSimulating, setIsSimulating] = useState(false);
  
  const calldata = encodeFunctionData({
    abi: contractAbi,
    functionName,
    args,
  });
  
  // Simulate on parameter change (with debounce)
  useEffect(() => {
    if (!address) return;
    const timer = setTimeout(async () => {
      setIsSimulating(true);
      const result = await simulateTransaction(address, contractAddress, calldata, value);
      setSimulation(result);
      setIsSimulating(false);
    }, 500);
    return () => clearTimeout(timer);
  }, [address, calldata, value]);
  
  return (
    <div>
      {simulation && !simulation.success && (
        <Alert variant="destructive">
          Transaction will fail with error: {simulation.revertReason}
        </Alert>
      )}
      {simulation?.assetChanges && (
        <SimulationPreview changes={simulation.assetChanges} />
      )}
      <button 
        disabled={isSimulating || simulation?.success === false}
        onClick={sendActualTransaction}
      >
        {isSimulating ? "Simulating..." : children}
      </button>
    </div>
  );
}

Simulation limitations

Simulation works with the current blockchain state. Between simulation and actual transaction, state can change:

  • AMM price changed (front-running, other trades)
  • Deadline expired
  • Allowance was used by another transaction

Solution: re-simulate quickly right before submit (< 1 second before) and warn if the result differs from the original. Also display the timestamp of the last simulation and a refresh button.

Alternative: Alchemy Simulation

Alchemy provides alchemy_simulateExecution and alchemy_simulateAssetChanges methods — a good alternative to Tenderly if already using Alchemy as an RPC provider:

const response = await fetch(ALCHEMY_RPC_URL, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    id: 1,
    jsonrpc: "2.0",
    method: "alchemy_simulateAssetChanges",
    params: [{ from, to, data: calldata, value: toHex(value) }],
  }),
});

Returns asset changes in an understandable format without needing to parse raw state diff.