Вызов state-changing функций — write
Как вызывать функции смарт-контракта, которые меняют State: подключить signer, отправить транзакцию и дождаться receipt.
Что такое write-вызов
Write-вызов меняет State Ethereum: записывает данные, переводит токены, создаёт NFT или вызывает другую функцию с побочным эффектом.
Такой вызов всегда является транзакцией. Его должен подписать signer, а отправитель платит gas.
Вызываем функцию increment()
Создайте проект:
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:
import { defineConfig } from "hardhat/config";
export default defineConfig({
solidity: {
version: "0.8.20",
},
});Создайте файл contracts/Counter.sol:
// 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:
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 в отдельном терминале:
npx hardhat nodeВо втором терминале выполните скрипт:
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-вызов превращается в подписанную транзакцию:
from: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
to: 0x...адрес Counter
data: 0xd09de08adata содержит selector функции increment(). Валидаторы выполняют этот вызов в EVM, меняют storage контракта и записывают receipt.
Если функция делает revert, изменения откатываются, но gas всё равно тратится.
Read vs write
Read-вызов:
const value = await counter.value();Он не требует подписи и не меняет State.
Write-вызов:
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 потрачен.
Что дальше
- Вызов view-функций — read — сравнить write-вызов с бесплатным чтением
- Статусы транзакции — pending, confirmed, failed — проверять результат после отправки hash
- Оценка газа (estimateGas) — заранее оценить стоимость write-вызова