A Rust-based blockchain indexer for NEAR protocol that extracts HOS contract interactions and stores them in PostgreSQL.
- Rust (1.70 or higher) - Install from rustup.rs
- PostgreSQL (12 or higher)
- Docker & Docker Compose (for containerized setup)
# Start with Docker
docker-compose up -d
# Or run locally
cargo run -- init # Initialize database
cargo run -- start # Start indexingThe docker-compose setup builds two containers:
postgres: PostgreSQL database (builds fromDockerfile.postgres)near-sink-sql: The indexer application (builds from mainDockerfile)
Edit config.toml for basic settings:
db_host = "localhost"
db_port = 5432
db_database = "near_indexer"
db_username = "postgres"
db_password = "password"
db_schema = "fastnear"
start_block = 183500000
num_threads = 64
# Important: Change this to trigger a backfill
app_version = "1.0.0"
hos_contract = "r-1745564650.testnet"The indexer only processes transactions that interact with contracts listed in hos_contracts, filtering out all other blockchain activity.
You can override any configuration setting using environment variables with the INDEXER_ prefix. Create a .env file in the project root or set environment variables directly:
# .env file example
INDEXER_DB_HOST=production-db.example.com
INDEXER_DB_PASSWORD=secure_password
INDEXER_START_BLOCK=184000000
INDEXER_APP_VERSION=1.0.1
INDEXER_LOG_LEVEL=debugEnvironment variables take precedence over config.toml values, making it easy to override settings for different deployments without modifying the configuration file.
The indexer tracks its progress using a cursor system that stores the last processed block for each app version. This enables reliable resumption after restarts and controlled backfilling when needed.
The cursors table maintains indexing state with:
- id: App version from configuration
- block_num: Last successfully processed block height
- block_id: Hash of the last processed block
When starting, the indexer determines which block to begin from using this precedence:
- CLI argument:
--start-block <number>(highest priority) - Database cursor: Last processed block for the current app version
- Configuration file:
start_blockvalue (fallback)
During normal operations (deployments without logic changes, VM restarts), the indexer automatically resumes from its last cursor position. This ensures continuous processing without gaps or duplicate work.
To reprocess historical data, you must manually increment the app_version in your configuration:
- Update version: Change
app_versioninconfig.toml(e.g., "1.0.0" → "1.0.1") - Set starting point: Update
start_blockto your desired starting block - Deploy: Restart the indexer with the new configuration
The new app version creates a separate cursor entry, allowing the indexer to process from your specified starting block while preserving the original processing state.
main.rs: Entry point, CLI argument parsing, command dispatchconfig.rs: Configuration loading from file and environment variablesdatabase.rs: PostgreSQL connection, table operations, cursor managementindexer.rs: Main indexing logic, block fetching, starting block determinationprocessor.rs: Receipt processing, data extraction, filtering HOS contracts
cargo run -- init- Initialize database by creating tables and viewscargo run -- start [--start-block <block>] [--num-threads <threads>]- Start indexing
The indexer creates analytical SQL views for querying governance and HOS data. All views are located in the sql_views/ directory.
proposals- Comprehensive governance proposal data with voting metadata, approval status, and vote countsregistered_voters- HOS token holders eligible to participate in governance votingproposal_voting_history- Individual vote records showing user voting patterns and choicesuser_activities- Summary of user interactions with HOS contracts and governancedelegation_events- Vote delegation actions and delegate relationshipsapproved_proposals- Proposals that have passed governance review and are eligible for public votingproposal_non_voters- Analysis of eligible voters who did not participate in specific proposals
The helper_queries/ directory contains utility functions:
safe_json_parse.sql- Parse JSON with error handling
-- Get all active proposals with vote counts
SELECT proposal_id, proposal_name, vote_count_for, vote_count_against
FROM proposals
WHERE proposal_status = 'Active';
-- Find top voters by participation
SELECT voter_account, COUNT(*) as votes_cast
FROM proposal_voting_history
GROUP BY voter_account
ORDER BY votes_cast DESC;
-- Check delegation relationships
SELECT delegator, delegate, delegation_timestamp
FROM delegation_events
ORDER BY delegation_timestamp DESC;For each of the respective data views there is a corresponding set of actions being performed on-chain. Some of this data is actually reflected in contract state. This table can be used to determine which view is an amalgamation of contract actions vs. having a direct relationship to contract storage. See this repo for the contracts.
| Data view | Contract State? | Storage Contract | Actions | Off-chain only |
|---|---|---|---|---|
| proposals | Yes | voting-contract |
create_proposal, get_proposal |
No |
| registered_voters | No | N/A | N/A | Yes |
| proposal_voting_history | No | N/A | vote |
Yes |
| user_activities | No | N/A | N/A | Yes |
| delegation_events | No | N/A | N/A | Yes |
| approved_proposals | Yes | voting-contract |
update |
No |
| proposal_non_voters | No | No | update |
Yes |
