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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка симулятора транзакций перед отправкой
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

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

«Почему мой газ улетел, а транзакция всё равно reverted?» — один из самых частых вопросов от пользователей DeFi. Большинство revert-ов предсказуемы: slippage exceeded, insufficient allowance, deadline passed. Симуляция транзакции перед отправкой решает эту проблему на корню и снижает количество failed транзакций до единиц.

Как работает симуляция

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

Три уровня глубины симуляции:

eth_call — базовый уровень. Возвращает return data или revert reason. Доступен на любой ноде, быстрый. Не показывает промежуточные состояния.

debug_traceCall — полный EVM trace: каждый OPCODE, storage reads/writes, внутренние calls. Требует ноду с debug API (Alchemy, Tenderly, или self-hosted Erigon). Медленный.

Tenderly Simulation API — наиболее полный результат из коробки: asset changes, state diff, event logs, gas breakdown. Платный, но значительно проще custom implementation.

Реализация через 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 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 через ABI
  if (error instanceof Error && "data" in error) {
    return decodeCustomError(error.data as `0x${string}`);
  }
  return "Unknown revert";
}

Tenderly Simulation API

Для production симуляторов с rich 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)
  • Deadline истёк
  • Allowance был использован другой транзакцией

Решение: повторная быстрая симуляция непосредственно перед submit (< 1 секунда до) и предупреждение если результат отличается от первоначального. Также отображаем timestamp последней симуляции и кнопку «обновить».

Альтернативы: 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.