китаев.tech

Вызов state-changing функций — write

Как вызывать функции смарт-контракта, которые меняют State: подключить signer, отправить транзакцию и дождаться receipt.

Что такое write-вызов

Write-вызов меняет State Ethereum: записывает данные, переводит токены, создаёт NFT или вызывает другую функцию с побочным эффектом.

Такой вызов всегда является транзакцией. Его должен подписать signer, а отправитель платит gas.


Вызываем функцию increment()

Создайте проект:

bash
mkdir call-write-functions-example
cd call-write-functions-example

npm init -y
npm pkg set type="module"
npm install ethers solc tsx
npm install --save-dev hardhat typescript @types/node
mkdir contracts scripts

Создайте файл hardhat.config.ts:

typescript
import { defineConfig } from "hardhat/config";

export default defineConfig({
  solidity: {
    version: "0.8.20",
  },
});

Создайте файл contracts/Counter.sol:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Counter {
    uint256 public value;

    event ValueChanged(uint256 newValue, address indexed caller);

    constructor(uint256 initialValue) {
        value = initialValue;
    }

    function increment() external {
        value += 1;
        emit ValueChanged(value, msg.sender);
    }
}

Создайте файл scripts/write-counter.ts:

typescript
import { ethers } from "ethers";
import solc from "solc";
import { readFileSync } from "node:fs";

const source = readFileSync("contracts/Counter.sol", "utf8");

const input = {
  language: "Solidity",
  sources: {
    "Counter.sol": {
      content: source,
    },
  },
  settings: {
    outputSelection: {
      "*": {
        "*": ["abi", "evm.bytecode.object"],
      },
    },
  },
};

const output = JSON.parse(solc.compile(JSON.stringify(input)));
const contract = output.contracts["Counter.sol"].Counter;

const abi = contract.abi;
const bytecode = `0x${contract.evm.bytecode.object}`;

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");

// Первый тестовый аккаунт Hardhat. Не используйте этот ключ в mainnet.
const signer = new ethers.Wallet(
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
  provider,
);

const factory = new ethers.ContractFactory(abi, bytecode, signer);
const counter = await factory.deploy(0);

await counter.waitForDeployment();

const address = await counter.getAddress();
const valueBefore = await counter.value();

console.log("Contract address:", address);
console.log("Value before:", valueBefore.toString());

const tx = await counter.increment();

console.log("Write transaction:", tx.hash);

const receipt = await tx.wait();

if (!receipt || receipt.status !== 1) {
  throw new Error("Транзакция increment() не была успешно выполнена");
}

const valueAfter = await counter.value();

console.log("Block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Value after:", valueAfter.toString());

Запустите локальную сеть Hardhat в отдельном терминале:

bash
npx hardhat node

Во втором терминале выполните скрипт:

bash
npx tsx scripts/write-counter.ts

Скрипт задеплоит Counter, вызовет increment() транзакцией и прочитает новое значение после подтверждения.


Как это работает

new ethers.Wallet(privateKey, provider) — создаёт signer. Он подписывает транзакцию от имени аккаунта.

new ethers.ContractFactory(abi, bytecode, signer) — деплоит контракт через signer, потому что deploy тоже меняет State.

counter.increment() — отправляет write-транзакцию. Это не локальный вызов и не eth_call.

tx.hash — hash отправленной транзакции. На этом этапе State ещё может не измениться.

tx.wait() — ждёт, пока транзакцию включат в блок, и возвращает receipt.

receipt.status — показывает результат выполнения. 1 означает success, 0 означает revert.

counter.value() после tx.wait() — read-вызов, который читает уже обновлённый State.


Что происходит в сети

Write-вызов превращается в подписанную транзакцию:

text
from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
to:   0x...адрес Counter
data: 0xd09de08a

data содержит selector функции increment(). Валидаторы выполняют этот вызов в EVM, меняют storage контракта и записывают receipt.

Если функция делает revert, изменения откатываются, но gas всё равно тратится.


Read vs write

Read-вызов:

typescript
const value = await counter.value();

Он не требует подписи и не меняет State.

Write-вызов:

typescript
const tx = await counter.increment();
await tx.wait();

Он требует signer, создаёт транзакцию и меняет State только после включения в блок.


Частые ошибки

Подключить контракт к provider и вызвать write-функцию → provider не умеет подписывать транзакции. Подключите контракт к signer.

Думать, что tx.hash означает успех → hash означает только отправку транзакции. Проверяйте результат через await tx.wait() и receipt.status.

Читать State сразу после отправки → до подтверждения блок может ещё не содержать вашу транзакцию. Сначала дождитесь tx.wait().

Игнорировать revert → транзакция может попасть в блок со статусом 0. В этом случае State не изменился, но gas потрачен.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.