Skip to main content
Events are the primary mechanism for smart contracts to communicate what happened on-chain. This tutorial covers emitting events in Solidity, querying them via JSON-RPC, and decoding them with ethers.js.

Emitting events in Solidity

pragma solidity ^0.8.20;

contract Vault {
    event Deposited(address indexed depositor, uint256 amount, uint256 timestamp);
    event Withdrawn(address indexed recipient, uint256 amount);

    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value, block.timestamp);
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "Transfer failed");
        emit Withdrawn(msg.sender, amount);
    }
}

Querying events with ethers.js

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://rpc1.autheo.com");

const abi = [
  "event Deposited(address indexed depositor, uint256 amount, uint256 timestamp)",
  "event Withdrawn(address indexed recipient, uint256 amount)"
];

const contract = new ethers.Contract("0x<contract-address>", abi, provider);

// Query last 10,000 blocks
const currentBlock = await provider.getBlockNumber();
const logs = await contract.queryFilter("Deposited", currentBlock - 10000, currentBlock);

logs.forEach(log => {
  const { depositor, amount, timestamp } = log.args;
  console.log(`Block ${log.blockNumber}: ${depositor} deposited ${ethers.formatEther(amount)} THEO`);
});

Filtering by indexed parameters

Indexed event parameters can be used as topics for efficient filtering:
// Filter Deposited events for a specific address
const filter = contract.filters.Deposited("0x<depositor-address>");
const logs = await contract.queryFilter(filter, -5000, "latest");
You can also filter with null to match any value:
// All Deposited events (any depositor)
const allDeposits = contract.filters.Deposited(null);

Raw eth_getLogs

For lower-level access or when you don’t have the ABI:
curl -X POST https://rpc1.autheo.com \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "eth_getLogs",
    "params": [{
      "fromBlock": "0x0",
      "toBlock": "latest",
      "address": "0x<contract-address>",
      "topics": ["0x<event-topic-hash>"]
    }],
    "id": 1
  }'
Compute the topic hash for an event signature:
const topic = ethers.id("Deposited(address,uint256,uint256)");
console.log(topic); // 0x...

Real-time event subscription

// Subscribe to new events as they are emitted
contract.on("Deposited", (depositor, amount, timestamp, event) => {
  console.log(`New deposit from ${depositor}: ${ethers.formatEther(amount)} THEO`);
  console.log(`Block: ${event.log.blockNumber}, Tx: ${event.log.transactionHash}`);
});

// Stop listening
contract.off("Deposited");
WebSocket connections are required for real-time subscriptions. Use wss://rpc1.autheo.com:8546 instead of the HTTP endpoint.

Decoding raw log data

If you have raw log data without the ABI:
const iface = new ethers.Interface([
  "event Deposited(address indexed depositor, uint256 amount, uint256 timestamp)"
]);

const rawLog = {
  topics: ["0x<topic0>", "0x<indexed-depositor>"],
  data: "0x<amount-and-timestamp-abi-encoded>"
};

const parsed = iface.parseLog(rawLog);
console.log("Depositor:", parsed.args.depositor);
console.log("Amount:", ethers.formatEther(parsed.args.amount));

Best practices

  • Index only parameters you need to filter on — indexed parameters cost more gas
  • Keep event payloads minimal; emit only what consumers need
  • Do not use events as the primary storage mechanism — use them for off-chain notification
  • Paginate eth_getLogs queries in blocks of 1,000–10,000 to avoid timeout errors

Next steps