Skip to content

Read Events

This guide demonstrates how to read and process events emitted by Starknet contracts using Starknet.go.

Prerequisites

  • Go 1.18 or higher
  • Starknet.go installed
  • A Starknet node URL
  • Contract address that emits events

Code Example

main.go
package main
 
import (
	"context"
	"fmt"
	"math/rand"
	"time"
 
	"github.com/NethermindEth/juno/core/felt"
	"github.com/NethermindEth/starknet.go/rpc"
	"github.com/NethermindEth/starknet.go/utils"
 
	setup "github.com/NethermindEth/starknet.go/examples/internal"
)
 
// main is the entry point of the program that demonstrates how to query Starknet events.
//
// This example shows how to:
// 1. Connect to a Starknet RPC provider
// 2. Query events with pagination using ChunkSize and ContinuationToken
// 3. Filter events by block range and contract address
// 4. Filter events by specific event keys
// 5. Combine multiple filters for precise event retrieval
//
// The program progressively applies more selective filters to demonstrate
// different ways of narrowing down event queries for efficient data retrieval.
func main() {
	// Load variables from '.env' file
	rpcProviderUrl := setup.GetRpcProviderUrl()
	wsProviderUrl := setup.GetWsProviderUrl()
 
	// Initialize connection to RPC provider
	provider, err := rpc.NewProvider(rpcProviderUrl)
	if err != nil {
		panic(fmt.Sprintf("Error dialing the RPC provider: %v", err))
	}
	fmt.Println("Established connection with the RPC provider")
 
	// Now we will call some functions to demonstrate the different filters we can apply.
	// Enter each function declaration to see the filters in action.
 
	// 1. call with ChunkSize and ContinuationToken
	callWithChunkSizeAndContinuationToken(provider)
	// 2. call with Block and Address filters
	callWithBlockAndAddressFilters(provider)
	// 3. call with Keys filter
	callWithKeysFilter(provider)
	// optional: filter with websocket
	filterWithWebsocket(provider, wsProviderUrl) // if the wsProviderUrl is empty, the websocket example will be skipped
 
	// after all, here is a call with all filters combined
	fmt.Println("\n ----- 4. all filters -----")
 
	contractAddress, err := utils.HexToFelt("0x1948e239f559bcbdf9388938a3c46bc79f52bcba7c4d5c9732568cb8eb6a53d") // a random contract address for our example
	if err != nil {
		panic(fmt.Sprintf("failed to create felt from the contract address, error %v", err))
	}
	key3, err := utils.HexToFelt("0x1bfc84464f990c09cc0e5d64d18f54c3469fd5c467398bf31293051bade1c39")
	if err != nil {
		panic(fmt.Sprintf("failed to create felt from the provided key, error %v", err))
	}
	key5, err := utils.HexToFelt("0x0")
	if err != nil {
		panic(fmt.Sprintf("failed to create felt from the provided key, error %v", err))
	}
 
	eventChunk, err := provider.Events(context.Background(), rpc.EventsInput{
		EventFilter: rpc.EventFilter{
			FromBlock: rpc.WithBlockNumber(660000), // from block 660000
			ToBlock:   rpc.WithBlockNumber(660100), // to block 660100
			Address:   contractAddress,             // sent from this contract address
			Keys: [][]*felt.Felt{
				// Here we are filtering all 'Transfer', 'Approval' and 'GameStarted' events.
				// (all events that have one of these selectors as the first key)
				{
					utils.GetSelectorFromNameFelt("Transfer"),
					utils.GetSelectorFromNameFelt("Approval"),
					utils.GetSelectorFromNameFelt("GameStarted"),
				},
				{},     // here we are saying that the second key is unconstrained, it can take any value
				{key3}, // the third key must be equal to key3
				{},     // the fourth key is also unconstrained
				{key5}, // the fifth key must be equal to key5
			},
		},
		ResultPageRequest: rpc.ResultPageRequest{
			ChunkSize: 1000,
		},
	}) // so this will return all events, between block 660000 and 660100, sent from the specified contract address,
	// that have one of the 'Transfer', 'Approval' or 'GameStarted' selectors as the first key,
	// and the third key is equal to key3, and the fifth key is equal to key5; the second and fourth keys can be any value
	if err != nil {
		panic(fmt.Sprintf("error retrieving events: %v", err))
	}
 
	fmt.Printf("number of returned events: %d\n", len(eventChunk.Events))
	fmt.Printf("block number of the first event: %d\n", eventChunk.Events[0].BlockNumber)
	fmt.Printf("block number of the last event: %d\n", eventChunk.Events[len(eventChunk.Events)-1].BlockNumber)
	randomEvent := eventChunk.Events[rand.Intn(len(eventChunk.Events))] // get a random event from the chunk
	fmt.Printf("random event block number: %d\n", randomEvent.BlockNumber)
	fmt.Printf("random event tx hash: %s\n", randomEvent.TransactionHash.String())
	fmt.Printf("random event sender address: %s\n", randomEvent.FromAddress.String())
	fmt.Printf("random event first key: %v\n", randomEvent.Keys[0].String())
	fmt.Printf("random event third key: %v\n", randomEvent.Keys[2].String())
	fmt.Printf("random event fifth key: %v\n", randomEvent.Keys[4].String())
}
 
func callWithChunkSizeAndContinuationToken(provider *rpc.Provider) {
	fmt.Println()
	fmt.Println(" ----- 1. call with ChunkSize and ContinuationToken -----")
 
	// The only required field is the 'ChunkSize' field, so let's fill it. This field is used
	// to limit the number of events returned in one call. If the number of events is greater than
	// the ChunkSize, the provider will return a continuation token in the 'ContinuationToken' field
	// that can be used to retrieve the next chunk.
	//
	// This will return 1000 events starting from the block 0.
	eventChunk, err := provider.Events(context.Background(), rpc.EventsInput{
		ResultPageRequest: rpc.ResultPageRequest{
			ChunkSize: 1000,
		},
	})
	if err != nil {
		panic(fmt.Sprintf("error retrieving events: %v", err))
	}
	fmt.Printf("number of returned events in the first chunk: %d\n", len(eventChunk.Events))
	fmt.Printf("block number of the first event in the first chunk: %d\n", eventChunk.Events[0].BlockNumber)
	fmt.Printf("block number of the last event in the first chunk: %d\n", eventChunk.Events[len(eventChunk.Events)-1].BlockNumber)
 
	// Now we will get the second chunk
	secondEventChunk, err := provider.Events(context.Background(), rpc.EventsInput{
		ResultPageRequest: rpc.ResultPageRequest{
			ChunkSize:         1000,
			ContinuationToken: eventChunk.ContinuationToken,
		},
	})
	if err != nil {
		panic(fmt.Sprintf("error retrieving events: %v", err))
	}
	fmt.Printf("number of returned events in the second chunk: %d\n", len(secondEventChunk.Events))
	fmt.Printf("block number of the first event in the second chunk: %d\n", secondEventChunk.Events[0].BlockNumber)
	fmt.Printf("block number of the last event in the second chunk: %d\n", secondEventChunk.Events[len(secondEventChunk.Events)-1].BlockNumber)
}
 
func callWithBlockAndAddressFilters(provider *rpc.Provider) {
	fmt.Println()
	fmt.Println(" ----- 2. call with Block and Address filters -----")
	contractAddress, err := utils.HexToFelt("0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7") // StarkGate: ETH Token
	if err != nil {
		panic(fmt.Sprintf("failed to create felt from the contract address, error %v", err))
	}
 
	fmt.Println("Contract Address: ", contractAddress.String())
 
	// We are using the following filters:
	// - FromBlock: The starting block number (inclusive)
	// - ToBlock: The ending block number (inclusive)
	// - Address: The contract address to filter events from
	//
	// So, we are filtering events from block 0 to block 100 and only from the provided contract address.
	eventChunk, err := provider.Events(context.Background(), rpc.EventsInput{
		EventFilter: rpc.EventFilter{
			FromBlock: rpc.WithBlockNumber(0),
			ToBlock:   rpc.WithBlockNumber(100),
			Address:   contractAddress,
		},
		ResultPageRequest: rpc.ResultPageRequest{
			ChunkSize: 1000,
		},
	})
	if err != nil {
		panic(fmt.Sprintf("error retrieving events: %v", err))
	}
	fmt.Printf("number of returned events: %d\n", len(eventChunk.Events))
	fmt.Printf("block number of the first event: %d\n", eventChunk.Events[0].BlockNumber)
	fmt.Printf("block number of the last event: %d\n", eventChunk.Events[len(eventChunk.Events)-1].BlockNumber)
	fmt.Printf("contract address of the first event: %s\n", eventChunk.Events[0].FromAddress.String())
}
 
func callWithKeysFilter(provider *rpc.Provider) {
	fmt.Println()
	fmt.Println(" ----- 3. call with Keys filter -----")
	fmt.Println(" --- step 1: filter all events with the 'Transfer' name ---")
 
	// Firstly, we need to understand how the 'keys' filter works.
	// (and of course, we need to know what 'keys' are, right? read more here:
	// 	https://book.cairo-lang.org/ch101-03-contract-events.html,
	// 	https://docs.starknet.io/architecture-and-concepts/smart-contracts/starknet-events/
	// )
	//
	// If the we send an event filter containing [[k_1, k_2], [], [k_3]], then the node should return
	// events whose first key is k_1 or k_2, the third key is k_3, and the second key is unconstrained and can take any value.
	// Ref: https://community.starknet.io/t/snip-13-index-transfer-and-approval-events-in-erc20s/114212
	//
	// The keys are interpreted as follows:
	// - The first key usually is the event selector
	// - The remaining keys will vary depending on the event
	//
	// So here we are filtering all 'Transfer' events (to be more precise, all events with the 'Transfer' selector as the first key)
	// from all addresses and contracts, from block 600000 to block 600100.
	eventChunk, err := provider.Events(context.Background(), rpc.EventsInput{
		EventFilter: rpc.EventFilter{
			FromBlock: rpc.WithBlockNumber(600000),
			ToBlock:   rpc.WithBlockNumber(600100),
			Keys: [][]*felt.Felt{
				{
					utils.GetSelectorFromNameFelt("Transfer"),
				},
			},
		},
		ResultPageRequest: rpc.ResultPageRequest{
			ChunkSize: 1000,
		},
	})
	// NOTE: An event can be nested in a Cairo component (See the Cairo code of the contract to verify).
	// In this case, the array of keys will start with additional hashes, and you will have to adapt your code in consequence
	// Ref: https://starknetjs.com/docs/guides/events#without-transaction-hash
	if err != nil {
		panic(fmt.Sprintf("error retrieving events: %v", err))
	}
 
	fmt.Printf("'Transfer' hash selector: %s\n", utils.GetSelectorFromNameFelt("Transfer").String())
 
	fmt.Printf("number of returned events: %d\n", len(eventChunk.Events))
	fmt.Printf("block number of the first event: %d\n", eventChunk.Events[0].BlockNumber)
	fmt.Printf("block number of the last event: %d\n", eventChunk.Events[len(eventChunk.Events)-1].BlockNumber)
	fmt.Printf("first key of the first event: %s\n", eventChunk.Events[0].Keys[0].String())
 
	fmt.Println()
	fmt.Println(" --- step 2: filter multiple events types ---")
 
	// Here we are filtering all 'Transfer', 'Approval' and 'GameStarted' events.
	eventChunk, err = provider.Events(context.Background(), rpc.EventsInput{
		EventFilter: rpc.EventFilter{
			FromBlock: rpc.WithBlockNumber(600000),
			ToBlock:   rpc.WithBlockNumber(600100),
			Keys: [][]*felt.Felt{
				// Notice that we are passing all selectors together in the same array, meaning that
				// the node will return events that match any of these values.
				// Also notice that the array is in the first position of the array, so basically
				// we are filtering all events that have one of these selectors as the first key.
				{
					utils.GetSelectorFromNameFelt("Transfer"),
					utils.GetSelectorFromNameFelt("Approval"),
					utils.GetSelectorFromNameFelt("GameStarted"),
				},
			},
		},
		ResultPageRequest: rpc.ResultPageRequest{
			ChunkSize: 1000,
		},
	})
	if err != nil {
		panic(fmt.Sprintf("error retrieving events: %v", err))
	}
 
	fmt.Printf("'Transfer' hash selector: %s\n", utils.GetSelectorFromNameFelt("Transfer").String())
	fmt.Printf("'Approval' hash selector: %s\n", utils.GetSelectorFromNameFelt("Approval").String())
	fmt.Printf("'GameStarted' hash selector: %s\n", utils.GetSelectorFromNameFelt("GameStarted").String())
 
	fmt.Printf("number of returned events: %d\n", len(eventChunk.Events))
	fmt.Printf("block number of the first event: %d\n", eventChunk.Events[0].BlockNumber)
	fmt.Printf("block number of the last event: %d\n", eventChunk.Events[len(eventChunk.Events)-1].BlockNumber)
	transferEvent := findEventInChunk(eventChunk, "Transfer")
	fmt.Printf("'Transfer' event found in block %d, tx hash: %s\n", transferEvent.BlockNumber, transferEvent.TransactionHash.String())
	gameStartedEvent := findEventInChunk(eventChunk, "GameStarted")
	fmt.Printf("'GameStarted' event found in block %d, tx hash: %s\n", gameStartedEvent.BlockNumber, gameStartedEvent.TransactionHash.String())
	approvalEvent := findEventInChunk(eventChunk, "Approval")
	fmt.Printf("'Approval' event found in block %d, tx hash: %s\n", approvalEvent.BlockNumber, approvalEvent.TransactionHash.String())
 
}
 
func filterWithWebsocket(provider *rpc.Provider, websocketUrl string) {
	if websocketUrl == "" {
		fmt.Println("\nNo websocket URL provided. Skipping websocket filter...")
		return
	}
 
	fmt.Println()
	fmt.Println(" ----- 4. filter with websocket -----")
 
	wsProvider, err := rpc.NewWebsocketProvider(websocketUrl)
	if err != nil {
		panic(fmt.Sprintf("error dialing the RPC provider: %v", err))
	}
	contractAddress, err := utils.HexToFelt("0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7") // StarkGate: ETH Token
	if err != nil {
		panic(fmt.Sprintf("failed to create felt from the contract address, error %v", err))
	}
 
	// Get the latest block number
	blockNumber, err := provider.BlockNumber(context.Background())
	if err != nil {
		panic(fmt.Sprintf("error getting the latest block number: %v", err))
	}
 
	// Create a channel to receive events
	eventsChan := make(chan *rpc.EmittedEvent)
 
	// Subscribe to events
	sub, err := wsProvider.SubscribeEvents(context.Background(), eventsChan, &rpc.EventSubscriptionInput{
		FromAddress: contractAddress,                       // only events from this contract address
		BlockID:     rpc.WithBlockNumber(blockNumber - 10), // Subscribe to events from the latest block minus 10 (it'll return
		// events from the last 10 blocks and progressively update as new blocks are added)
		Keys: [][]*felt.Felt{
			// the 'keys'filter behaves the same way as the RPC provider `starknet_getEvents` explained above.
			// So this will return all events that have the 'Transfer' selector as the first key.
			{
				utils.GetSelectorFromNameFelt("Transfer"),
			},
		},
	})
	if err != nil {
		panic(fmt.Sprintf("error subscribing to events: %v", err))
	}
 
	fmt.Println("Successfully subscribed to events")
 
	// Read events from the channel
	for {
		select {
		case event := <-eventsChan:
			// This case will be triggered when a new event is received.
			fmt.Printf("New event received: Block %d, Event tx hash: %s\n", event.BlockNumber, event.TransactionHash.String())
		case err := <-sub.Err():
			// This case will be triggered when an error occurs.
			panic(err)
		case <-time.After(5 * time.Second):
			// stop the loop after 5 seconds
			fmt.Println("Exiting...")
			return
		}
	}
 
}
 
// simple function to find an event by name in a chunk of events
func findEventInChunk(eventChunk *rpc.EventChunk, eventName string) rpc.EmittedEvent {
	selector := utils.GetSelectorFromNameFelt(eventName)
 
	for _, event := range eventChunk.Events {
		if event.Keys[0].String() == selector.String() {
			return event
		}
	}
	return rpc.EmittedEvent{}
}

Explanation

  1. Initialize the Starknet client
  2. Define the contract address
  3. Create an event filter with desired parameters
  4. Fetch events using the filter
  5. Process and display event data

Best Practices

  • Use appropriate block ranges
  • Filter events by specific keys
  • Handle pagination for large result sets
  • Consider using WebSocket for real-time events
  • Store event data for future reference

Common Issues

  • Invalid contract address
  • Network connectivity issues
  • Large result sets
  • Event format mismatch
  • Block range too large