Developer Fundamentals

-- V. Events and Logs

Learn how to filter, retrieve, and decode smart contract logs on VeChainThor for both real-time and historical data.

Accessing Events and Logs

Events are signals emitted from smart contracts. Every event is logged, immutable, and accessible using a blockchain node only. Smart contracts cannot access events themselves.

Client applications can either listen to events and act accordingly or use logged events to access historical data.

The example uses a public contract and paginates through the results.

Example Data

Similar to the previous lesson, the example used below utilizes the VTHO contract, which manages VeChain's VTHO Token.

contract.filters.<EventName>(output)

The filters provide a simple way to access events in a human-readable way and to populate log requests with the correct criteria.

For example, a filter for allTransfers can be created using:

const allTransfers = vtho.filters.Transfer()
const filteredTransfers = vtho.filters.Transfer(<from>, <to>

Another example filters for all transfers to a specific address:

const filteredTransfers = vtho.filters.Transfer(null, <to>

<to> is the second output of the ABI. Unwanted filters can be skipped by passing null.

To receive the logs from the blockchain, the built filter object provides a .get() function. Calling it will return the list of matching events.

const allTransfers = vtho.filters.Transfer()
const result = await allTransfers.get()

For pagination, there are three optional parameters that allow filtering for a specific range, paginating, and ordering the result set:

  • unit ('block' | 'time') - Specifies the unit of measurement for fetching data.

  • from & to (number) - Defines the range for fetching data.

  • offset & limit (number) - Controls pagination of the results.

.get(
 { unit?: 'block' | 'time', from?: number, to?: number },
 { offset?: number, limit?: number },
 'asc' | 'desc'
)

.get() returns a list of results because it can support multiple requests as well.

The data is available in both raw and decoded forms:

const transfers = vtho.filters
  // pass filters in the order of the input definition for the event
  // skip values by passing null or undefined
  .Transfer(null, '0x0000000000000000000000000000456e65726779');

const results = await transfers.get(null, { limit: 2})

results.forEach(result => {
    result.forEach(log => {
        // raw log data
        console.log(log)

        // access to decoded data
        console.log("Transfer", log.decodedData._from, log.decodedData._to, log.decodedData._value)
    })
})

Check How It Works

This example shows a full read request putting this lesson together.

With filterEventLogs(), logs for multiple events can be requested in a single request, improving network performance and simplifying interaction.

For example, requesting VTHO Transfers from and to an address in one request:

const results = await thor.logs.filterEventLogs({
  criteriaSet: [
    ...vtho.filters.Transfer(ZERO_ADDRESS).criteriaSet, // FROM Zero
    ...vtho.filters.Transfer(null, ZERO_ADDRESS).criteriaSet, // TO Zero
  ],
  range: {
    unit: 'block',
    from: 1_000_000,
    to: 20_000_000,
  },
});

results.forEach((result) => {
  result.forEach((log) => {
    console.log(
      'Transfer',
      log.decodedData._from,
      log.decodedData._to,
      log.decodedData._value
    );
  });
});

Request Logs

Due to the potentially large amount of log entries, it is essential to implement filtering and pagination mechanisms for efficient data access.

To access and retrieve logs, the logs.filterRawEventLogs function allows filtering based on a specified range and enables pagination by utilizing offset and limits.

The snippet below illustrates the process of retrieving transfer events for VTHO tokens. Initially, the event that requires filtering must be encoded into a byte format:

import { ABIEvent } from '@vechain/sdk-core';

const event = new ABIEvent(
  'event Transfer(address indexed from, address indexed to, uint256 amount)'
);
const encodedTopics = event.encodeFilterTopics([])

The indexed variables can be filtered directly in the filter request, providing fast access to a subset of information. The list argument on the encoding can provide them.

Our example will filter for the second variable to:

const encodedTopics = event.encodeFilterTopics([
  // first indexed value is "from", set to null to not use it
  null,

  // second indexed value is "to"
  '0x0000000000000000000000000000456e65726779',
]);

With the encoded version logs.filterRawEventLogs can be called to return all matching logs:

const filteredLogs = await thor.logs.filterRawEventLogs({
  criteriaSet: [
    // filter by address and topics, empty topics are ignored
    {
      address: '0x0000000000000000000000000000456e65726779',
      topic0: encodedTopics[0],
      topic1: encodedTopics[1],
      topic2: encodedTopics[2],
      topic3: encodedTopics[3],
      topic4: encodedTopics[4],
    },
  ]

Pagination options ({ offset?: number, limit?: number }) and order ('asc' | 'desc') can be passed as an additional parameter. Additionally, the logs can be restricted to a certain block range by defining a range ({ unit?: 'block' | 'time', from?: number, to?: number }).

The response needs to be decoded using the event definition to gain access to all information. event.decodeEventLog(log) can be used on a selected event or, in our example, on all returned logs:

const decodedLogs = filteredLogs.map(log => event.decodeEventLog(log))

Each decoded log will have an attribute for each event variable, like decodedLog.from, and can alternatively be accessed as a list in the order of the event parameter definition (decodedLog[0] equals from).

Test It Yourself

The snippet below shows how you can make a request to the blockchain and the name of a specific contract:

import { ThorClient } from '@vechain/sdk-network';

const thor = ThorClient.at('https://mainnet.vechain.org');

const filteredLogs = await thor.logs.filterRawEventLogs({
  criteriaSet: [
    {
      address: '0x0000000000000000000000000000456e65726779',
    },
  ],
  range: {
    unit: 'block',
    from: 0,
    to: 10000000,
  },
  options: { 
    offset: 0, 
    limit: 3 
  },
  'asc'
);

console.log('Results', filteredLogs);