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>().
Copy Neo.Plugins.EventFilter.dll and config.json into:
neo-cli/Plugins/Neo.Plugins.EventFilter/
The plugin registers a live-updating dashboard in the neo-cli console:
neo> filter status
Press q to return to the prompt.
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. |
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>();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}");
}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.
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}");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}");
}
}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.
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
}
]
}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 } }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.
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.
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
}
}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. |
| 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. |
| 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. |
Licensed under the Apache License, Version 2.0.