Работа с событиями: watchContractEvent и getLogs
Прочитаешь исторические Transfer через getLogs и подпишешься на новые события WETH через watchContractEvent.
История и live — два разных API
События контракта (Transfer, Approval, …) попадают в logs блоков. getLogs читает уже записанную историю за диапазон блоков. watchContractEvent подписывается на новые логи по мере появления блоков — замена contract.on("Transfer", ...) из ethers.
Для live нужен WebSocket-транспорт (или HTTP с polling). История работает и через обычный http().
История: getLogs за последние блоки
import {
createPublicClient,
http,
formatEther,
parseAbiItem,
} from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({
chain: mainnet,
transport: http("https://ethereum.publicnode.com"),
});
const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const latestBlock = await publicClient.getBlockNumber();
const fromBlock = latestBlock - 50n;
const logs = await publicClient.getLogs({
address: wethAddress,
event: parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)",
),
fromBlock,
toBlock: latestBlock,
});
console.log(`Блоки ${fromBlock} → ${latestBlock}, логов: ${logs.length}`);
for (const log of logs.slice(0, 5)) {
console.log({
from: log.args.from,
to: log.args.to,
value: formatEther(log.args.value ?? 0n),
tx: log.transactionHash,
});
}log.args уже декодирован — вручную через Interface.parseLog ходить не нужно.
Live: watchContractEvent
import {
createPublicClient,
webSocket,
formatEther,
} from "viem";
import { mainnet } from "viem/chains";
const publicClient = createPublicClient({
chain: mainnet,
transport: webSocket("wss://ethereum.publicnode.com"),
});
const wethAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const transferAbi = [
{
type: "event",
name: "Transfer",
inputs: [
{ name: "from", type: "address", indexed: true },
{ name: "to", type: "address", indexed: true },
{ name: "value", type: "uint256", indexed: false },
],
},
] as const;
let received = 0;
const maxEvents = 3;
console.log("Ждём новые WETH Transfer...");
const unwatch = publicClient.watchContractEvent({
address: wethAddress,
abi: transferAbi,
eventName: "Transfer",
onLogs: (logs) => {
for (const log of logs) {
received += 1;
console.log({
from: log.args.from,
to: log.args.to,
value: formatEther(log.args.value ?? 0n),
tx: log.transactionHash,
});
if (received >= maxEvents) {
unwatch();
console.log("Подписка закрыта");
}
}
},
onError: (error) => {
console.error("Ошибка подписки:", error);
},
});unwatch() снимает подписку. Без него скрипт будет слушать бесконечно.
Фильтр по indexed-полям:
publicClient.watchContractEvent({
address: wethAddress,
abi: transferAbi,
eventName: "Transfer",
args: {
from: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
},
onLogs: (logs) => console.log(logs),
});Как это работает
getLogs({ address, event, fromBlock, toBlock }) — RPC eth_getLogs. parseAbiItem("event Transfer(...)") задаёт сигнатуру: viem сам считает topic0 и декодирует data/topics в args.
watchContractEvent({ abi, eventName, onLogs }) — при WebSocket это eth_subscribe на логи; при http() viem сам поллит новые блоки. Для dApp с постоянным UI бери webSocket.
fromBlock / toBlock — bigint. Широкий диапазон на публичном RPC часто режется лимитом ноды — начинай с десятков/сотен блоков.
args.from / args.to в фильтре работают только для indexed параметров события. value у ERC-20 Transfer не indexed — отфильтровать по сумме на уровне RPC нельзя.
Если ты знаешь ethers.js
| ethers v6 | viem |
|---|---|
provider.getLogs({ topics }) + iface.parseLog | getLogs({ event }) → сразу log.args |
contract.on("Transfer", callback) | watchContractEvent({ eventName, onLogs }) |
contract.off / removeAllListeners | функция unwatch(), которую вернул watch |
WebSocketProvider | transport: webSocket(url) у createPublicClient |
callback (from, to, value, event) | onLogs: (logs) => ... — массив логов за тик |
Частые ошибки
Огромный fromBlock → latest на public RPC → нода отвечает ошибкой лимита. Сужай окно или используй архивный/платный endpoint, либо ходи чанками.
HTTP-клиент и ожидание «мгновенного» push → на http() подписка — polling, задержка в секунды нормальна. Для стрима нужен webSocket.
Забыл unwatch() → в скриптах процесс не завершится, в браузере утечёт подписка при размонтировании компонента.
Что дальше
- Автовывод типов из ABI — как
as constтипизируетargsсобытий и функций - Мультивызовы: multicall — пачка read-вызовов вместо серии RPC
- Исторические события через getLogs — тот же сценарий на ethers.js v6