Розробка симулятора транзакцій перед відправкою

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка симулятора транзакцій перед відправкою
Середній
~3-5 днів
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1284
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1196
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    901
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1119
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    586
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    853

Розробка симулятора транзакцій перед відправкою

«Чому мій газ улетів, а транзакція все одно reverted?» — один з найчастіших питань від користувачів DeFi. Більшість revert-ів передбачувані: перевищений slippage, недостатньо дозволу, дедлайн минув. Симуляція транзакції перед відправкою вирішує цю проблему в корені та зменшує кількість невдалих транзакцій до одиниць.

Як працює симуляція

Ethereum нода дозволяє вигляді eth_call або debug_traceCall — виконати транзакцію проти поточного (або історичного) стану блокчейна без фактичної відправки. Отримайте результат: success/revert + revert reason + state changes + gas usage.

Три рівні глибини симуляції:

eth_call — базовий рівень. Повертає return data або revert reason. Доступний на будь-якій ноді, швидкий. Не показує проміжні стани.

debug_traceCall — повний EVM trace: кожен OPCODE, storage reads/writes, внутрішні виклики. Потребує ноду з debug API (Alchemy, Tenderly, або self-hosted Erigon). Повільно.

Tenderly Simulation API — найбільш повний результат з коробки: asset changes, state diff, event logs, gas breakdown. Платний, але значно простіший, ніж користувацька реалізація.

Реалізація через 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) {
    // Розбір причини revert
    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;
  }
  // Декодування користувацької помилки через ABI
  if (error instanceof Error && "data" in error) {
    return decodeCustomError(error.data as `0x${string}`);
  }
  return "Unknown revert";
}

Tenderly Simulation API

Для production симуляторів із багатим UX, Tenderly надає значно більше інформації:

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", // Ціна газу не важлива для симуляції
        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,
  };
}

Розбір asset changes для UX

Користувачам потрібно бачити не raw state diff, а ясне резюме:

interface AssetChange {
  type: "ERC20" | "ERC721" | "ETH";
  direction: "in" | "out";
  amount: string;
  symbol: string;
  tokenAddress?: string;
  tokenId?: string; // для 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}`;
  });
}

// Результат у UI:
// - 0.5 ETH
// + 1500 USDC
// - NFT #4521 (BAYC)

Інтеграція в компонент TransactionButton

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,
  });
  
  // Симулюємо при зміні параметрів (з 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">
          Транзакція завершиться з помилкою: {simulation.revertReason}
        </Alert>
      )}
      {simulation?.assetChanges && (
        <SimulationPreview changes={simulation.assetChanges} />
      )}
      <button 
        disabled={isSimulating || simulation?.success === false}
        onClick={sendActualTransaction}
      >
        {isSimulating ? "Симулюємо..." : children}
      </button>
    </div>
  );
}

Обмеження симуляції

Симуляція працює з поточним станом блокчейна. Між симуляцією та реальною транзакцією стан може змінитися:

  • Ціна AMM змінилася (front-running, інші trades)
  • Дедлайн минув
  • Дозволено було використано іншою транзакцією

Рішення: пересимулюйте швидко прямо перед submit (< 1 секунди до) та попередьте, якщо результат відрізняється від оригіналу. Також відображайте часову мітку останної симуляції та кнопку refresh.

Альтернатива: Alchemy Simulation

Alchemy надає методи alchemy_simulateExecution та alchemy_simulateAssetChanges — гарна альтернатива Tenderly, якщо вже використовуєте Alchemy як RPC провайдер:

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) }],
  }),
});

Повертає asset changes у зрозумілому форматі без необхідності розбору raw state diff.