Skip to content

AxLabs/neo-event-filter-plugin

Repository files navigation

Neo.Plugins.EventFilter

Indexes smart contract notifications (events) from the Neo N3 blockchain and exposes an EVM-style filter/query API. Other plugins or application code running in the same process can discover the service via NeoSystem.GetService<IEventFilterService>().

Installation

Copy Neo.Plugins.EventFilter.dll and config.json into:

neo-cli/Plugins/Neo.Plugins.EventFilter/

CLI

The plugin registers a live-updating dashboard in the neo-cli console:

neo> filter status

Press q to return to the prompt.

Configuration

config.json — all fields are optional and fall back to their defaults:

{
  "PluginConfiguration": {
    "StoragePath": "EventFilterData",
    "EnableHistoricalSync": true,
    "ReplayBatchSize": 1000,
    "ReplayStartBlock": 0,
    "MaxResultsPerQuery": 10000,
    "FilterTimeoutSeconds": 300,
    "MaxInstalledFilters": 100,
    "ExceptionPolicy": "StopPlugin"
  }
}
Key Default Description
StoragePath EventFilterData Directory (relative to plugin root) for the event index store.
EnableHistoricalSync true Replay historical blocks on startup to back-fill notifications.
ReplayBatchSize 1000 Blocks between replay commits.
ReplayStartBlock 0 First block to replay (ignored when resuming).
MaxResultsPerQuery 10000 Hard cap on events returned by a single GetLogs call.
FilterTimeoutSeconds 300 Installed filters are evicted after this many seconds without a poll.
MaxInstalledFilters 100 Maximum number of concurrently installed filters.

API Usage

Obtaining the service

The plugin registers itself as an IEventFilterService on the NeoSystem service container when the node starts.

// From any code running inside the same neo-cli process (e.g. another plugin):
var eventFilter = system.GetService<IEventFilterService>();

One-shot queries with GetLogs

GetLogs scans the index and returns matching events immediately — no subscription required.

// All indexed events (up to MaxResultsPerQuery)
var all = eventFilter.GetLogs(new EventFilter());

// Events from a specific contract
var neoTransfers = eventFilter.GetLogs(new EventFilter
{
    Contract = NativeContract.NEO.Hash,
    EventName = "Transfer",
});

// Events in a block range
var recent = eventFilter.GetLogs(new EventFilter
{
    FromBlock = 5_000_000,
    ToBlock   = 5_001_000,
});

// Combine contract + event name + block range + limit
var mintEvents = eventFilter.GetLogs(new EventFilter
{
    Contract  = UInt160.Parse("0xd2a4cff31913016155e38e474a2c06d08be276cf"),
    EventName = "Mint",
    FromBlock = 4_000_000,
    ToBlock   = 5_000_000,
    Limit     = 500,
});

foreach (var evt in mintEvents)
{
    Console.WriteLine(
        $"Block {evt.BlockIndex}  TX {evt.TxHash}  " +
        $"{evt.ContractHash}::{evt.EventName}  " +
        $"state bytes: {evt.State.Length}");
}

Streaming new events with install/poll/uninstall

For long-running consumers that need to react to new events as they arrive, use the filter subscription pattern. This is the Neo equivalent of eth_newFilter / eth_getFilterChanges.

// 1. Install a filter — returns a filter ID.
//    The filter remembers the current indexed height as its cursor.
var filterId = eventFilter.InstallFilter(new EventFilter
{
    Contract  = NativeContract.GAS.Hash,
    EventName = "Transfer",
});

// 2. Poll periodically — returns only events indexed since the last poll.
while (running)
{
    var newEvents = eventFilter.GetFilterChanges(filterId);

    foreach (var evt in newEvents)
    {
        // Process each new Transfer event ...
    }

    await Task.Delay(TimeSpan.FromSeconds(5));
}

// 3. Uninstall when done.
eventFilter.UninstallFilter(filterId);

Filters that are not polled within FilterTimeoutSeconds (default 300 s) are automatically evicted.

Checking index status

var status = eventFilter.GetIndexStatus();

Console.WriteLine($"Last indexed block : {status.LastIndexedBlock}");
Console.WriteLine($"Replay in progress : {status.ReplayInProgress}");
Console.WriteLine($"Replayed up to     : {status.ReplayedUpTo}");
Console.WriteLine($"Live indexed from  : {status.LiveIndexedFrom}");

Working with FilteredEvent.State

The State field contains the notification arguments serialized with Neo.SmartContract.BinarySerializer. Deserialize it back into a Neo.VM.Types.StackItem to inspect individual arguments:

using Neo.SmartContract;

foreach (var evt in eventFilter.GetLogs(new EventFilter { EventName = "Transfer" }))
{
    var state = BinarySerializer.Deserialize(evt.State, 1024 * 1024, 4096);

    if (state is Neo.VM.Types.Array args && args.Count >= 3)
    {
        var from   = args[0].IsNull ? "mint" : new UInt160(args[0].GetSpan()).ToString();
        var to     = args[1].IsNull ? "burn" : new UInt160(args[1].GetSpan()).ToString();
        var amount = args[2].GetInteger();
        Console.WriteLine($"Transfer {from} -> {to}: {amount}");
    }
}

JSON-RPC API

The plugin automatically registers five JSON-RPC methods with the RpcServer plugin. Requires the RpcServer plugin to be loaded alongside EventFilter.

Parameters are positional (standard JSON-RPC params array). Trailing parameters can be omitted to use defaults.

geteventfilterlogs

Query indexed events.

Position Name Type Default Description
0 contract string "" (all) Contract script hash to filter by.
1 eventName string "" (all) Event name to filter by.
2 fromBlock uint 0 (start) Start of block range (inclusive).
3 toBlock uint 0 (latest) End of block range (inclusive). 0 = last indexed.
4 limit int 0 (default 1000) Max events to return. 0 = config default.
# NEO Transfer events in a block range
curl -X POST http://localhost:10332 -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1,
  "method": "geteventfilterlogs",
  "params": ["0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5", "Transfer", 5000000, 5001000, 100]
}'

# All indexed events (no filters)
curl -X POST http://localhost:10332 -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1,
  "method": "geteventfilterlogs",
  "params": []
}'

# Only by event name (skip contract with empty string)
curl -X POST http://localhost:10332 -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1,
  "method": "geteventfilterlogs",
  "params": ["", "Transfer"]
}'

Response:

{
  "jsonrpc": "2.0", "id": 1,
  "result": [
    {
      "blockindex": 5000001,
      "txhash": "0xabc123...",
      "contract": "0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5",
      "eventname": "Transfer",
      "state": "DAEAAQ==",
      "index": 0
    }
  ]
}

installeventfilter

Install a persistent filter whose cursor starts at the current indexed height. Same parameters as geteventfilterlogs.

curl -X POST http://localhost:10332 -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1,
  "method": "installeventfilter",
  "params": ["0xd2a4cff31913016155e38e474a2c06d08be276cf", "Transfer"]
}'

Response:

{ "jsonrpc": "2.0", "id": 1, "result": { "filterId": 1 } }

geteventfilterchanges

Returns events indexed since the last poll for this filter, then advances the cursor.

Position Name Type Description
0 filterId uint Filter ID from installeventfilter.
curl -X POST http://localhost:10332 -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1,
  "method": "geteventfilterchanges",
  "params": [1]
}'

Response: same array format as geteventfilterlogs. Returns [] when no new events have been indexed since the last poll.

uninstalleventfilter

Remove a previously installed filter.

Position Name Type Description
0 filterId uint Filter ID to remove.
curl -X POST http://localhost:10332 -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1,
  "method": "uninstalleventfilter",
  "params": [1]
}'

Response:

{ "jsonrpc": "2.0", "id": 1, "result": { "uninstalled": true } }

Returns false if the filter was already removed or expired.

geteventfilterstatus

Returns the current indexing status. Takes no parameters.

curl -X POST http://localhost:10332 -H 'Content-Type: application/json' -d '{
  "jsonrpc": "2.0", "id": 1,
  "method": "geteventfilterstatus",
  "params": []
}'

Response:

{
  "jsonrpc": "2.0", "id": 1,
  "result": {
    "replayedUpTo": 5000000,
    "liveIndexedFrom": 5000001,
    "lastIndexedBlock": 5123456,
    "replayInProgress": false
  }
}

Event object format

Each event returned by geteventfilterlogs and geteventfilterchanges:

Field Type Description
blockindex uint Block that produced the notification.
txhash string? Transaction hash, or null for system notifications.
contract string Contract script hash (e.g. "0xef4073..." ).
eventname string Notification name (e.g. "Transfer").
state string Base64-encoded BinarySerializer payload.
index uint Notification index within the block.

EventFilter properties

Property Type Default Description
FromBlock uint? null (= 0) Start of the block range (inclusive).
ToBlock uint? null (= last indexed) End of the block range (inclusive).
Contract UInt160? null (= all) Only return events from this contract. Uses the secondary index for fast lookups.
EventName string? null (= all) Only return events with this name (e.g. "Transfer").
Limit int 1000 Maximum events to return. Capped server-side by MaxResultsPerQuery.

FilteredEvent properties

Property Type Description
BlockIndex uint Block that produced the notification.
TxHash UInt256? Transaction hash (null for system notifications like OnPersist).
ContractHash UInt160 Contract that emitted the notification.
EventName string Notification event name.
State byte[] BinarySerializer-encoded notification arguments.
NotificationIndex ushort Position of this notification within the block.

License

Licensed under the Apache License, Version 2.0.

About

Bring EVM-style filter/query API to Neo N3

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages