Skip to content

v0.6.0#14

Merged
nman98 merged 39 commits into
Cogwheel-Validator:mainfrom
nman98:main
Mar 15, 2026
Merged

v0.6.0#14
nman98 merged 39 commits into
Cogwheel-Validator:mainfrom
nman98:main

Conversation

@nman98

@nman98 nman98 commented Mar 14, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • API key management CLI, key generation, in-memory keystore with periodic refresh, and API key auth support.
    • IP- and API-key rate limiting middleware with headers, Retry-After, and fail-open behavior; Valkey client integration.
    • New analytics endpoints: block/tx counts & volumes (daily/hourly), daily active accounts, validator signing, last-X transactions, cursor pagination, and base64↔base64url utilities.
    • Continuous aggregates and materialized views for counters.
  • Chores

    • Docker compose, config examples, env vars and Makefile adjusted for Valkey/rate limiting and key DB.
    • CI/workflow and dependency updates; docs and data-model expanded.

nman98 added 27 commits March 5, 2026 21:31
…ctures. Updated descriptions for height range validation and modified response types to use pointers for block data. Introduced helper methods in transaction handler for cleaner code and improved error handling.
… privilege validation. Introduce AllTableNames function to streamline privilege assignment for users.
…gregate views and adjust continuous aggregate SQL generation with chunk interval.
…ethods for getting data from the view tables.
…thods for transaction and block data retrieval. Update address, block, and transaction handlers to use the new interfaces.
…action counts, refactor route registration, and enhance input validation for address and transaction types.
…amps instead of dates for improved precision in different timezones and consistency in block and transaction count queries.
…ogic. Replace log.Fatalf with structured logging and simplify address management functions for better clarity and performance.
@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@nman98 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c363d7d2-fce2-4962-a8f7-1325bfed5300

📥 Commits

Reviewing files that changed from the base of the PR and between 0b49258 and 210629a.

📒 Files selected for processing (1)
  • go.mod
📝 Walkthrough

Walkthrough

Adds API key management, valkey-based rate limiting and keystore, new analytics endpoints and continuous-aggregate support, many DB read/query additions, handler/interface refactors, route wiring, CLI/key tooling, server bootstrap changes, and large indexer/materialize and SQL datatype additions.

Changes

Cohort / File(s) Summary
Config & Env
\.env.example, config-api.yml.example, api/config/loader.go, api/config/types.go
Added VALKEY_* and KEY_DB_* env vars; new ApiConfig fields (disable_rate_limit, ip_rpm_limit, key_refresh_interval, trusted_proxies) and Valkey env loader.
API Key CLI & Generation
api/key_cmd.go, api/key/generate.go
New Cobra key subcommand (create/disable/enable/list), DB flags/password prompt, and key generator returning raw key, prefix and SHA-256 hash.
Keystore (in-memory cache)
api/keystore/keystore.go, api/keystore/keystore_test.go
KeyStore with RWMutex, Refresh() from DB, StartPeriodicRefresh(), and tests with fake DB.
Valkey client & Rate limiter
api/valkey/client.go, api/ratelimit/ratelimit.go, api/ratelimit/ratelimit_test.go
Valkey client wrapper and RateLimiter middleware using Valkey + KeyStore interfaces; real-IP/trusted-proxy handling, headers, 429/Retry-After, fail-open behavior and tests.
Handlers & Interfaces
api/handlers/interface.go, api/handlers/*.go (address.go,blocks.go,transactions.go,validators.go)
Replaced DatabaseHandler with specialized interfaces (Address/Block/Transaction/Validator); handlers updated and new analytics endpoints (counts, volumes, daily active, validator signing).
Huma API types
api/huma-types/*.go (address.go,block.go,transaction.go,validator.go)
Added Date type, many new input/output types for totals and ranges, adjusted pointer/slice outputs and validation tags for new endpoints.
Routes registration
api/routes/*.go (addresses.go,blocks.go,transactions.go,validators.go,utils.go)
New Register*Routes helpers wiring handlers to endpoints with operation metadata.
Server bootstrap & CLI wiring
api/main.go, api/serve.go
Moved run logic to serve.go, registered keyCmd, server init: DB pool, keystore refresh, valkey client, rate limiter middleware, routes, OpenAPI, TLS/CORS.
DB key management & pool
pkgs/database/key_mgmt.go, pkgs/database/pool.go
DB methods for inserting/listing/enabling/disabling keys and limits; TimescaleDb.Close() added.
DB read/query additions
pkgs/database/query_view.go, pkgs/database/query_block.go, pkgs/database/insert.go, pkgs/database/types_api.go
New TimescaleDb read methods for counts/volumes/daily-active/validator signing; concurrent block+tx fetch helpers; BlockData TxCount→TxCounter rename; new domain types.
SQL/Data types & Materialize
pkgs/sql_data_types/*.go, indexer/db_init/*.go, pkgs/sql_data_types/table.go
New hypertable structs and materialize types, continuous-aggregate SQL generation and policies (columnstore), ApiKey table type added, AppointPrivileges extended for "keymgr".
Indexer: processor, decoder, orchestrator, address cache
indexer/data_processor/*, indexer/decoder/*, indexer/orchestrator/*, indexer/address_cache/*
Large refactors: modularized block/tx/message processing, added TotalMsgCount, corrected TrasnactionsData→TransactionsData, improved address-cache sync/retry with tests.
Compression training
compression/train/process.go
Extracted fetchTransactionBatch and processEvents; removed BuildZstdDict.
Routes & docs & docker & CI
docs/*, docker-compose*.yml, Makefile, .github/workflows/*, CHANGELOG.md, README.md
Docs for new endpoints & counters, docker-compose valkey service and env vars, Makefile target changes, new govulncheck workflow, release/build updates, changelog entry.
Tests & mocks
api/handlers/database_test.go, api/handlers/blocks_test.go, api/ratelimit/ratelimit_test.go, ...
Extended mocks/tests across handlers, ratelimit, keystore, and indexer components to exercise new endpoints and flows.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant RL as RateLimiter
    participant KS as KeyStore
    participant VK as Valkey
    participant DB as KeyDB

    Client->>RL: HTTP request (may include X-API-Key)
    RL->>KS: GetKeyLimit(hash) / lookup
    KS-->>RL: (limit, found?)
    alt API key provided & valid
        RL->>VK: Increment(identifier, ctx)
        VK-->>RL: (count, err)
        alt first increment
            RL->>VK: Expirer(identifier, window)
            VK-->>RL: (ok, err)
        end
        alt count > limit
            RL-->>Client: 429 + Retry-After
        else within limit
            RL-->>Client: forward request + X-RateLimit-* headers
        end
    else API key invalid
        RL-->>Client: 401 Unauthorized
    end
    Note over KS,DB: KS periodically Refresh() -> DB.GetAllApiKeysWithLimits()
Loading
sequenceDiagram
    participant Client
    participant Handler
    participant DB
    participant MV as MaterializedView

    Client->>Handler: Request with date-range
    Handler->>Handler: validate input (dates/ranges)
    alt invalid
        Handler-->>Client: 400 Bad Request
    else valid
        Handler->>DB: GetBlockCountByDate(start,end)
        DB->>MV: read continuous aggregate
        MV-->>DB: aggregated rows
        DB-->>Handler: []*BlockCountByDate
        alt empty
            Handler-->>Client: 404 Not Found
        else
            Handler-->>Client: 200 OK + data
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • v0.5.0 #12: Strong overlap with handlers, analytics endpoints, valkey/keystore, and DB query changes.
  • v0.4.0 #11: Related DB/view methods and handler signature changes that intersect with this PR.
  • v0.3.0 #9: Earlier DatabaseHandler and config/loader work that this PR further subdivides and extends.

Poem

🐰 I dug a key from random bytes so bright,

I cached it close and refreshed at night,
Valkey counts hops and the headers show,
Aggregates hum where the tallies grow,
A rabbit cheers — the API takes flight!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

v0.6.0: Add rate limiting, API key management, and transaction analytics with comprehensive refactoring

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• **API Rate Limiting**: Added comprehensive rate limiting middleware using Valkey with support for
  both IP-based and API key-based limiting, including proper HTTP headers and distributed request
  counting
• **API Key Management**: Implemented complete API key management system with CRUD operations, CLI
  commands, key generation, and periodic refresh mechanism with database synchronization
• **Transaction & Block Statistics**: Added new analytics endpoints for transaction counts, volumes,
  and block statistics with time-range queries and proper validation
• **Database Optimization**: Refactored concurrent processing in data processor with helper
  functions, optimized block queries with parallel data fetching, and added continuous aggregate SQL
  generation utilities
• **Code Refactoring**: Extracted large functions into smaller focused helpers across multiple files
  (operator.go, setup.go, main.go, decoder.go), improved error handling with structured logging, and
  split monolithic DatabaseHandler interface into specialized interfaces
• **TimescaleDB Improvements**: Added materialized view data types, continuous aggregate query
  methods, updated compression configuration to newer API, and implemented aggregate refresh
  functionality
• **Testing**: Added comprehensive test coverage for address cache, rate limiter middleware, and
  keystore functionality
• **Bug Fixes**: Fixed multiple typos (TrasnactionsDataTransactionsData, speciflyspecify), updated reflection API usage for Go 1.22+ compatibility, and corrected privilege
  parameter naming
• **Configuration & Dependencies**: Added Valkey service to Docker Compose, updated Go dependencies
  (added decimal, valkey-glide, removed gozstd), and enhanced environment configuration for rate
  limiting
Diagram
flowchart LR
  A["API Requests"] -->|"Rate Limiting"| B["Valkey Client"]
  A -->|"API Key Auth"| C["KeyStore"]
  C -->|"Periodic Sync"| D["API Key DB"]
  E["Indexer"] -->|"Process Blocks"| F["Refactored Operator"]
  F -->|"Concurrent Processing"| G["Database"]
  H["Analytics Queries"] -->|"Aggregates"| I["Materialized Views"]
  I -->|"Query Methods"| J["TimescaleDB"]
  K["CLI Commands"] -->|"Manage Keys"| D
Loading

Grey Divider

File Changes

1. indexer/data_processor/operator.go ✨ Enhancement +425/-396

Refactor concurrent processing with helper functions and improve error handling

• Refactored concurrent processing by extracting helper functions (extractAddresses,
 processBlock, processTransaction, processValidatorSigning, transactionDecoding, decodeTx,
 processMessageGroup) to reduce code duplication and improve maintainability
• Replaced mutex-based aggregation in createAddressTx with sequential processing using
 pre-allocated slices, eliminating concurrent goroutines for better performance
• Fixed typo: TrasnactionsDataTransactionsData throughout the file
• Improved error handling by replacing log.Printf with structured logging using l.Error() with
 caller and stack information
• Removed unused imports (crypto/sha256, log) and simplified transaction hash processing

indexer/data_processor/operator.go


2. pkgs/sql_data_types/table.go Refactoring +51/-437

Reorganize table definitions and add API key management

• Removed time import as it's no longer needed in this file
• Moved all hypertable struct definitions (Blocks, ValidatorBlockSigning, AddressTx,
 TransactionGeneral, MsgSend, MsgCall, MsgAddPackage, MsgRun) to new file hypertables.go
• Added new ApiKey struct for API key management with fields for ID, prefix, hash, name, RPM
 limit, and active status
• Added helper functions AllTableNames() and AllAggrTableNames() to return lists of table names
 for database initialization
• Simplified DBTable and GnoMessage interfaces for better code organization

pkgs/sql_data_types/table.go


3. pkgs/sql_data_types/hypertables.go Refactoring +455/-0

New file with hypertable struct definitions

• New file containing all hypertable struct definitions previously in table.go
• Includes Blocks, ValidatorBlockSigning, AddressTx, TransactionGeneral, MsgSend, MsgCall,
 MsgAddPackage, and MsgRun structs with their methods
• Maintains all database mapping information, table names, and address collection methods
• Removed Txs field from Blocks struct (no longer stored in database)

pkgs/sql_data_types/hypertables.go


View more (56)
4. indexer/cmd/setup.go ✨ Enhancement +274/-122

Add refresh-aggregates command and refactor database setup

• Added new refreshAggregatesCmd command to force full refresh of continuous aggregate views
• Refactored createDbCmd into smaller helper functions (createDatabaseSetup,
 getFlagStringWithDefault, checkCurrentDatabase, initializeNewDatabase, createDatabaseTypes,
 createRegularTables, createHypertables, createContinuousAggregates) for better code
 organization
• Added support for ApiKey table creation in regular tables setup
• Enhanced createUserCmd to support keymgr privilege level and pass table names to
 AppointPrivileges
• Fixed typo: speciflyspecify in createConfigCmd help text

indexer/cmd/setup.go


5. api/handlers/transactions.go ✨ Enhancement +236/-68

Add transaction analytics handlers and refactor message handling

• Changed DatabaseHandler interface to TransactionDbHandler for better semantic clarity
• Extracted message type handling into helper methods (getBankSendResponse, getMsgCallResponse,
 getMsgAddPackageResponse, getMsgRunResponse) to reduce code duplication
• Added new handler methods for transaction analytics: GetLastXTransactions, GetTotalTxCount24h,
 GetTotalTxCountByDate, GetTotalTxCountByHour, GetVolumeByDate, GetVolumeByHour
• Added input validation for date/time ranges in new analytics methods
• Improved error handling with more specific error messages

api/handlers/transactions.go


6. api/key_cmd.go ✨ Enhancement +287/-0

New API key management command-line interface

• New file implementing API key management commands (create, disable, enable, list)
• Provides CLI interface for managing API keys with database connectivity
• Includes keyDbParams struct for database configuration and helper functions for password
 prompting and database connection
• Supports environment variables for database credentials (KEY_DB_HOST, KEY_DB_PORT,
 KEY_DB_USER, KEY_DB_NAME, KEY_DB_PASSWORD)
• Displays generated API keys with prefix and full key material for user storage

api/key_cmd.go


7. indexer/decoder/types.go ✨ Enhancement +4/-3

Add message count tracking to transaction data

• Added TotalMsgCount field to BasicTxData struct to track total message count in a transaction
• Improved struct field alignment and formatting for better readability

indexer/decoder/types.go


8. pkgs/database/query_view.go ✨ Enhancement +397/-0

Add TimescaleDB continuous aggregation query methods

• New file with 397 lines implementing 11 query methods for TimescaleDB views
• Methods retrieve aggregated data: block counts, transaction counts, volumes, validator signing
 metrics
• Supports querying by date ranges, hourly intervals, and 24-hour windows
• Uses time bucketing and gap-filling for continuous aggregation queries

pkgs/database/query_view.go


9. api/main.go Refactoring +3/-243

Refactor API main.go to extract serve logic

• Extracted large Run function from rootCmd into separate runServe function
• Removed direct imports of HTTP, router, and handler packages from main file
• Added keyCmd command registration in init() function
• Simplified main.go to focus on command structure

api/main.go


10. pkgs/sql_data_types/materialize.go ✨ Enhancement +334/-0

Add SQL data types for materialized views

• New file with 334 lines defining continuous aggregation data types
• Implements 5 materialized view structs: TxCounter, FeeVolume, DailyActiveAccounts,
 ValidatorSigningCounter, BlockCounter
• Each struct includes table metadata tags and aggregation policy configuration
• Provides helper functions for reflection-based column/function extraction

pkgs/sql_data_types/materialize.go


11. pkgs/database/query_block.go ✨ Enhancement +172/-47

Optimize block queries with concurrent data fetching

• Refactored block query methods to use concurrent fetching with errgroup
• Split block and transaction data fetching into separate queries executed in parallel
• Added helper methods fetchBlocksData and fetchTransactionData for code reuse
• Removed inline transaction array aggregation from blocks table

pkgs/database/query_block.go


12. indexer/address_cache/address_test.go 🧪 Tests +244/-0

Add comprehensive address cache tests

• New test file with 244 lines covering AddressSolver functionality
• Tests include: caching behavior, batch insert with retry, one-by-one fallback, validator flag
 handling
• Mock database implementation for testing insert/fetch operations
• Comprehensive edge case coverage: empty input, partial success, retry attempts

indexer/address_cache/address_test.go


13. indexer/address_cache/address.go Refactoring +89/-91

Refactor address cache with structured logging

• Refactored AddressSolver method into 4 smaller focused functions for clarity
• Replaced log package with structured logger from pkgs/logger
• Simplified constructor by removing redundant if/else branches
• Added helper methods: findUncached, syncExistingFromDB, insertWithRetry, insertOneByOne,
 fetchAndCacheInserted

indexer/address_cache/address.go


14. indexer/db_init/materialize.go ✨ Enhancement +238/-0

Add continuous aggregate SQL generation utilities

• New file with 238 lines implementing continuous aggregate SQL generation and management
• Defines interfaces ContinuousAggregateDefinition and LateralJoiner for aggregate configuration
• Provides methods to create views, alter columnstore settings, add policies, and refresh aggregates
• Generates SQL using reflection-based column ordering and GROUP BY ordinal positions

indexer/db_init/materialize.go


15. api/ratelimit/ratelimit_test.go 🧪 Tests +212/-0

Add rate limiter middleware tests

• New test file with 212 lines testing rate limiting middleware
• Tests cover: IP-based limiting, API key validation, rate limit headers, 429 responses
• Includes fake implementations of ValkeyLike and KeyStoreLike interfaces
• Tests header priority for real IP detection (X-Forwarded-For, X-Real-Ip, RemoteAddr)

api/ratelimit/ratelimit_test.go


16. api/serve.go Refactoring +202/-0

Extract API server initialization to serve.go

• New file with 202 lines containing extracted runServe function from main.go
• Initializes database, keystore, rate limiter, and Valkey client
• Configures CORS with X-API-Key header support
• Registers all API routes and starts HTTP/HTTPS server

api/serve.go


17. indexer/decoder/decoder.go Refactoring +31/-20

Refactor transaction message decoding logic

• Extracted message processing logic into separate processMsgs function
• Pre-allocated messages slice with known capacity instead of appending
• Changed message assignment from append to direct index assignment
• Simplified documentation comments and removed TODO items

indexer/decoder/decoder.go


18. api/handlers/blocks.go ✨ Enhancement +44/-46

Add block statistics handlers and simplify responses

• Changed DatabaseHandler interface to BlockDbHandler for clarity
• Removed manual struct field mapping in responses, now returns database objects directly
• Added two new handler methods: GetBlockCount24h and GetBlockCountByDate
• Added validation for date range queries (max 30 days) and height range queries (max 100 blocks)

api/handlers/blocks.go


19. api/keystore/keystore_test.go 🧪 Tests +147/-0

Add keystore unit tests

• New test file with 147 lines testing KeyStore functionality
• Tests cover: empty store, key retrieval, refresh from database, error propagation
• Includes concurrent access testing for read lock safety
• Mock database implementation for testing key loading

api/keystore/keystore_test.go


20. compression/train/process.go Refactoring +83/-43

Refactor compression training process functions

• Extracted transaction batch fetching into separate fetchTransactionBatch function
• Extracted event processing into separate processEvents function
• Moved BuildZstdDict function definition before processEvents for logical ordering
• Improved code organization and reusability of helper functions

compression/train/process.go


21. pkgs/database/key_mgmt.go ✨ Enhancement +185/-0

Add API key management database operations

• New file with 185 lines implementing API key management database operations
• Provides CRUD operations: InsertApiKey, GetAllApiKeys, GetAllApiKeysWithLimits,
 ListApiKeys
• Implements key state management: DisableKey, EnableKey, DisableKeyByName, EnableKeyByName
• Includes AdjustRpmLimit for modifying rate limit per key

pkgs/database/key_mgmt.go


22. api/huma-types/transaction.go ✨ Enhancement +50/-0

Add transaction statistics API types

• Added 6 new input/output type pairs for transaction and volume statistics endpoints
• New types: LastXTransactionsGetInput/Output, TotalTxCount24hGetInput/Output,
 TxCountByDateGetInput/Output
• Added volume query types: VolumeByDateGetInput/Output, VolumeByHourGetInput/Output
• All new types include proper validation constraints and documentation

api/huma-types/transaction.go


23. api/handlers/interface.go ✨ Enhancement +33/-5

Refactor database handler into specialized interfaces

• Split monolithic DatabaseHandler interface into four specialized interfaces: AddressDbHandler,
 ValidatorDbHandler, BlockDbHandler, and TransactionDbHandler
• Added new methods for statistics: GetDailyActiveAccount, GetValidatorSigning24h,
 GetValidatorSigningByHour, GetBlockCount24h, GetBlockCountByDate, and transaction volume/count
 methods
• Removed comments separating sections as they are now represented by separate interfaces

api/handlers/interface.go


24. api/ratelimit/ratelimit.go ✨ Enhancement +113/-0

Add rate limiting middleware with Valkey support

• New rate limiting middleware implementation using Valkey for distributed request counting
• Supports both API key-based and IP-based rate limiting with configurable RPM limits
• Implements proper HTTP headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) for
 rate limit information
• Includes helper function realIP to extract client IP from various proxy headers

api/ratelimit/ratelimit.go


25. indexer/db_init/tables.go 🐞 Bug fix +7/-10

Fix reflection API usage and add key manager privilege

• Changed reflect.Ptr to reflect.Pointer for Go 1.22+ compatibility in two locations
• Fixed typo: renamed parameter privilage to privilege in AppointPrivileges method
• Added new privilege type keymgr for API key management with specific table grants
• Removed validation check for privilege type (now handled elsewhere)

indexer/db_init/tables.go


26. api/handlers/database_test.go 🧪 Tests +57/-0

Add mock implementations for new database methods

• Added mock implementations for new database handler methods: GetTotalTxCount24h,
 GetTotalTxCountByDate, GetTotalTxCountByHour, GetVolumeByDate, GetVolumeByHour
• Added mock implementations for block statistics methods: GetBlockCount24h, GetBlockCountByDate
• Added mock implementation for GetDailyActiveAccount method

api/handlers/database_test.go


27. api/routes/transactions.go ✨ Enhancement +52/-0

Add transaction routes registration

• New file registering transaction-related API routes with Huma framework
• Includes endpoints for transaction retrieval, statistics (count by day/hour), and volume metrics
• Each route includes descriptive summaries and documentation for API consumers

api/routes/transactions.go


28. indexer/orchestrator/operator.go 🐞 Bug fix +5/-5

Fix typo in transactions data type name

• Fixed typo: renamed TrasnactionsData to TransactionsData in method signature and return types
• Updated all references to use correct spelling in collectTransactionsFromBlocks and processAll
 methods

indexer/orchestrator/operator.go


29. api/handlers/validators.go ✨ Enhancement +59/-0

Add validators handler with signing endpoints

• New handler for validator-related endpoints with two methods
• GetValidatorSigning24h returns signing performance for last 24 hours
• GetValidatorSigningByHour returns hourly signing data with validation for time range (max 7
 days)

api/handlers/validators.go


30. api/config/types.go ⚙️ Configuration changes +8/-0

Add rate limiting and Valkey configuration types

• Added rate limiting configuration fields: IpRpmLimit and KeyRefreshInterval to ApiConfig
• Added new ValkeyEnv struct for Valkey connection configuration with host and port

api/config/types.go


31. pkgs/database/types_api.go ✨ Enhancement +33/-1

Add new database response types for statistics

• Added import for github.com/shopspring/decimal package
• Fixed field name: TxCount renamed to TxCounter in BlockData struct
• Added new types: BlockCountByDate, DailyActiveAccount, TxCountTimeRange, VolumeByDenom,
 DenomVolume, ValidatorSigning

pkgs/database/types_api.go


32. api/routes/blocks.go ✨ Enhancement +45/-0

Add blocks routes registration

• New file registering block-related API routes with Huma framework
• Includes endpoints for block retrieval and statistics (count by day/hour)
• Each route includes descriptive summaries and documentation

api/routes/blocks.go


33. api/huma-types/block.go ✨ Enhancement +22/-5

Update block types and add statistics endpoints

• Changed response body types from values to pointers for consistency: BlockData, BlockSigners,
 []database.BlockData
• Added new input/output types for block statistics: BlockCount24hGetInput/Output,
 BlockCountByDateGetInput/Output

api/huma-types/block.go


34. api/keystore/keystore.go ✨ Enhancement +60/-0

Add API key store with periodic refresh

• New key store implementation for managing API keys in memory with database synchronization
• Provides thread-safe access to API key limits via GetKeyLimit method
• Includes periodic refresh mechanism to sync keys from database at configurable intervals

api/keystore/keystore.go


35. api/handlers/blocks_test.go 🧪 Tests +2/-2

Update block handler tests for pointer types

• Updated test assertions to dereference pointer response bodies for GetAllBlockSigners and
 GetLatestBlock
• Adjusts to match new pointer-based response types

api/handlers/blocks_test.go


36. api/handlers/address.go ✨ Enhancement +21/-2

Update address handler with daily active accounts

• Changed DatabaseHandler to AddressDbHandler in type and constructor
• Added new GetDailyActiveAccount method with input validation for date range (max 30 days)

api/handlers/address.go


37. api/huma-types/address.go ✨ Enhancement +10/-1

Add address validation and daily active accounts types

• Added validation constraints to Address field: minLength:"40" maxLength:"40"
• Added new input/output types for daily active accounts endpoint

api/huma-types/address.go


38. api/valkey/client.go ✨ Enhancement +43/-0

Add Valkey client implementation

• New Valkey client wrapper implementing the ValkeyLike interface
• Provides Increment and Expirer methods for rate limiting operations
• Configurable connection and request timeouts

api/valkey/client.go


39. api/huma-types/validator.go ✨ Enhancement +25/-0

Add validator endpoint types

• New file with input/output types for validator endpoints
• Includes types for 24-hour signing data and hourly signing data with time range parameters

api/huma-types/validator.go


40. api/routes/addresses.go ✨ Enhancement +24/-0

Add addresses routes registration

• New file registering address-related API routes with Huma framework
• Includes endpoints for address transactions and daily active addresses statistics

api/routes/addresses.go


41. api/routes/validators.go ✨ Enhancement +19/-0

Add validators routes registration

• New file registering validator-related API routes with Huma framework
• Includes endpoints for validator signing performance (24h and hourly)

api/routes/validators.go


42. api/routes/utils.go ✨ Enhancement +19/-0

Add utility routes for base64 conversion

• New file registering utility routes for Base64/Base64URL conversion
• Provides POST endpoints for encoding and decoding operations

api/routes/utils.go


43. indexer/orchestrator/operator_test.go 🐞 Bug fix +2/-2

Fix typo in mock data processor

• Fixed typo: updated mock method signatures from TrasnactionsData to TransactionsData
• Updated ProcessTransactions and ProcessMessages method signatures

indexer/orchestrator/operator_test.go


44. api/config/loader.go ✨ Enhancement +14/-0

Add Valkey environment configuration loader

• Added new LoadValkeyEnvironment function to load Valkey configuration from environment variables
• Follows same pattern as existing environment loaders

api/config/loader.go


45. indexer/orchestrator/types.go 🐞 Bug fix +2/-2

Fix typo in data processor interface

• Fixed typo: renamed TrasnactionsData to TransactionsData in DataProcessor interface methods

indexer/orchestrator/types.go


46. indexer/db_init/hypertable.go ⚙️ Configuration changes +3/-3

Update TimescaleDB compression configuration

• Updated TimescaleDB compression settings to use newer API: timescaledb.enable_columnstore
 instead of timescaledb.compress
• Changed timescaledb.compress_segmentby to timescaledb.segmentby and
 timescaledb.compress_orderby to timescaledb.orderby

indexer/db_init/hypertable.go


47. indexer/decoder/product.go ✨ Enhancement +10/-0

Add total message count getter method

• Added new GetTotalMsgCount method to DecodedMsg struct
• Returns the total message count from the decoded message's basic data

indexer/decoder/product.go


48. api/key/generate.go ✨ Enhancement +19/-0

Add API key generation utility

• New file with GenerateApiKey function for creating API keys
• Generates cryptographically secure random keys with SHA256 hashing
• Returns raw key, key prefix, and key hash for storage

api/key/generate.go


49. indexer/data_processor/types.go 🐞 Bug fix +1/-1

Fix typo in transactions data struct

• Fixed typo: renamed struct TrasnactionsData to TransactionsData

indexer/data_processor/types.go


50. go.sum Dependencies +14/-10

Update dependencies for rate limiting and decimals

• Added dependencies: github.com/shopspring/decimal v1.4.0 and
 github.com/valkey-io/valkey-glide/go/v2 v2.2.7
• Removed dependency: github.com/valyala/gozstd v1.24.0
• Updated OpenTelemetry dependencies to newer versions

go.sum


51. go.mod Dependencies +10/-8

Update Go module dependencies

• Added new dependencies: github.com/klauspost/compress, github.com/shopspring/decimal,
 github.com/valkey-io/valkey-glide/go/v2
• Removed github.com/valyala/gozstd dependency
• Updated OpenTelemetry packages to versions 1.42.0
• Moved github.com/cosmos/gogoproto from indirect to direct dependency

go.mod


52. CHANGELOG.md 📝 Documentation +28/-0

Add version 0.5.0 release notes

• Added new version 0.5.0 release notes dated 2026-03-05
• Documents zstd compression, API improvements, cursor pagination, Base64 utilities, and Docker
 support
• Lists added features, changes, and dependency updates

CHANGELOG.md


53. docker-compose-dev.yml ⚙️ Configuration changes +17/-0

Add Valkey service to dev docker-compose

• Added Valkey service configuration with Alpine image and health checks
• Added VALKEY_HOST and VALKEY_PORT environment variables to API service
• Updated API service dependencies to include Valkey

docker-compose-dev.yml


54. docker-compose.yml ⚙️ Configuration changes +17/-0

Add Valkey service to production docker-compose

• Added Valkey service configuration with Alpine image and health checks
• Added VALKEY_HOST and VALKEY_PORT environment variables to API service
• Updated API service dependencies to include Valkey

docker-compose.yml


55. .env.example ⚙️ Configuration changes +12/-1

Add Valkey and key management environment variables

• Added Valkey configuration variables: VALKEY_HOST and VALKEY_PORT
• Added key management database connection variables for API key CLI commands

.env.example


56. Makefile ⚙️ Configuration changes +1/-1

Update API build target path

• Changed build target from api/main.go to ./api for building the entire API package

Makefile


57. config-api.yml.example ⚙️ Configuration changes +7/-1

Add rate limiting configuration to example

• Added X-API-Key to cors_allowed_headers
• Added rate limiting configuration: ip_rpm_limit and key_refresh_interval

config-api.yml.example


58. indexer/cmd/live.go Additional files +0/-3

...

indexer/cmd/live.go


59. pkgs/database/insert.go Additional files +0/-1

...

pkgs/database/insert.go


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Empty blocks slice panic🐞 Bug ⛯ Reliability
Description
pkgs/database.(*TimescaleDb).GetBlock and GetLatestBlock index blocks[0] without verifying any rows
were returned, which will panic when the requested block (or any block for a chain) does not exist.
This can crash the API process on otherwise valid 404-style requests.
Code

pkgs/database/query_block.go[R61-64]

+	if txs[blocks[0].Height] != nil {
+		blocks[0].Txs = append(blocks[0].Txs, txs[blocks[0].Height]...)
+	}
+	return blocks[0], nil
Evidence
GetBlock/GetLatestBlock access blocks[0] after fetchBlocksData, but fetchBlocksData returns an empty
slice (and nil error) when the query yields no rows, so blocks[0] panics.

pkgs/database/query_block.go[22-65]
pkgs/database/query_block.go[67-112]
pkgs/database/query_block.go[239-257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GetBlock` and `GetLatestBlock` can panic due to indexing `blocks[0]` when `fetchBlocksData` returns an empty slice.
### Issue Context
This happens when the blocks query returns no rows (e.g., querying a non-existent height or an empty chain).
### Fix Focus Areas
- pkgs/database/query_block.go[22-65]
- pkgs/database/query_block.go[67-112]
- pkgs/database/query_block.go[239-257]
### Suggested fix
- After `eg.Wait()`, add:
- `if len(blocks) == 0 { return nil, fmt.Errorf("block not found") }`
- Consider using `QueryRow` for single-block queries and mapping `pgx.ErrNoRows` to a not-found error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. LastX blocks txs truncated🐞 Bug ✓ Correctness
Description
pkgs/database.(*TimescaleDb).GetLastXBlocks fetches transactions with LIMIT $2 where $2 is the
number of blocks requested, which truncates the total transaction set and yields
incomplete/non-deterministic tx lists for those blocks. As a result, block responses can omit most
transactions when blocks contain more than one transaction.
Code

pkgs/database/query_block.go[R138-145]

+	query2 := `
+	SELECT 
+	encode(tx_hash, 'base64'),
+	block_height
+	FROM transaction_general
+	WHERE chain_name = $1
+	LIMIT $2
+	`
Evidence
The tx query limits the total number of returned tx rows to the number of requested blocks, rather
than fetching all txs for those blocks, so the join-by-height map is incomplete.

pkgs/database/query_block.go[127-173]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GetLastXBlocks` truncates transactions due to `LIMIT $2` on `transaction_general`, where `$2` is the number of blocks.
### Issue Context
Blocks can contain multiple transactions; limiting total tx rows to the number of blocks makes responses incorrect.
### Fix Focus Areas
- pkgs/database/query_block.go[127-174]
### Suggested fix
- Replace tx query with one that fetches all txs for returned heights, e.g.:
- `WITH last_blocks AS (SELECT height FROM blocks WHERE chain_name=$1 ORDER BY height DESC LIMIT $2)
   SELECT encode(tx_hash,'base64'), block_height
   FROM transaction_general tg
   JOIN last_blocks lb ON tg.block_height = lb.height
   WHERE tg.chain_name=$1;`
- Keep assembling `txs[block_height]` as today.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Block tx_count never set 🐞 Bug ✓ Correctness
Description
BlockData.TxCounter is never populated: fetchBlocksData scans only hash/height/timestamp/chain_id
and no code computes TxCounter after appending transactions. Consequently, API responses will report
tx_count=0 regardless of the actual number of transactions.
Code

pkgs/database/query_block.go[R249-256]

  	block := &BlockData{}
-		err := rows.Scan(&block.Hash, &block.Height, &block.Timestamp, &block.ChainID, &block.Txs)
+		err := rows.Scan(&block.Hash, &block.Height, &block.Timestamp, &block.ChainID)
  	if err != nil {
  		return nil, err
  	}
  	blocks = append(blocks, block)
  }
-	return blocks, nil
+	return blocks, rows.Err()
Evidence
BlockData has a tx_count field, but the block query scan does not populate it and handlers return
the struct directly without computing it from Txs.

pkgs/database/types_api.go[10-18]
pkgs/database/query_block.go[247-256]
api/handlers/blocks.go[23-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`tx_count` is always the zero value because it is never scanned or computed.
### Issue Context
Handlers now return `*database.BlockData` directly, so missing population of `TxCounter` affects all block endpoints.
### Fix Focus Areas
- pkgs/database/query_block.go[239-277]
- pkgs/database/query_block.go[22-65]
- pkgs/database/query_block.go[67-112]
- pkgs/database/query_block.go[127-174]
- pkgs/database/query_block.go[190-237]
### Suggested fix
- After appending tx hashes:
- set `blocks[0].TxCounter = len(blocks[0].Txs)` in single-block methods
- set `block.TxCounter = len(block.Txs)` in loops for multi-block methods
- Consider adding a test to assert tx_count matches len(txs).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
4. Validator hourly query mismatch🐞 Bug ✓ Correctness
Description
pkgs/database.(*TimescaleDb).GetValidatorSigningByHour filters on vsc.validator_id = $2 but passes
validatorAddress (string) as the $2 argument. This will cause query type errors or consistently
empty results for the hourly validator signing endpoint.
Code

pkgs/database/query_view.go[R373-380]

+	WHERE vsc.chain_name    = $1
+		AND vsc.validator_id  = $2
+		AND vsc.time_bucket >= $3 AND vsc.time_bucket <= $4
+	GROUP BY time_bucket('1 hour', time_bucket)
+	ORDER BY time DESC
+	`
+	rows, err := t.pool.Query(ctx, query, chainName, validatorAddress, date1, date2)
+	if err != nil {
Evidence
GetValidatorSigning24h resolves the address to an integer validatorId and uses it in the query, but
GetValidatorSigningByHour does not; it passes the address string into a predicate expecting
validator_id.

pkgs/database/query_view.go[306-342]
pkgs/database/query_view.go[355-380]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GetValidatorSigningByHour` passes a string address into a SQL filter expecting an integer `validator_id`.
### Issue Context
This breaks `/validators/{validator_address}/signing/hourly`.
### Fix Focus Areas
- pkgs/database/query_view.go[355-396]
- pkgs/database/query_view.go[306-342]
### Suggested fix
- Add an address-&amp;gt;id lookup (reuse the `query1` logic from `GetValidatorSigning24h`).
- Call `t.pool.Query(ctx, query, chainName, validatorId, date1, date2)`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. EnableKey hash encoding bug🐞 Bug ✓ Correctness
Description
pkgs/database.(*TimescaleDb).EnableKey passes a [32]byte directly as a query argument for a bytea
column, unlike other functions that pass hash[:]. This can fail pgx encoding and prevent enabling
API keys.
Code

pkgs/database/key_mgmt.go[R155-160]

+func (t *TimescaleDb) EnableKey(ctx context.Context, hash [32]byte) error {
+	result, err := t.pool.Exec(ctx, `
+		UPDATE api_keys
+		SET is_active = true
+		WHERE hash = $1
+		`, hash)
Evidence
DisableKey correctly uses hash[:] for the hash bytea column; EnableKey uses hash which is a
different Go type and may not encode as bytea.

pkgs/database/key_mgmt.go[140-146]
pkgs/database/key_mgmt.go[155-161]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`EnableKey` uses `hash` instead of `hash[:]` for a bytea SQL parameter.
### Issue Context
Other code paths treat `hash` as bytea slices, so this inconsistency can break enabling keys.
### Fix Focus Areas
- pkgs/database/key_mgmt.go[155-168]
### Suggested fix
- Change the Exec call to pass `hash[:]`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Spoofable IP rate limiting🐞 Bug ⛨ Security
Description
api/ratelimit.realIP trusts X-Forwarded-For and X-Real-Ip headers from any client, allowing
attackers to spoof arbitrary IPs to bypass IP-based rate limiting or to rate-limit other users. This
undermines the intended protection for unauthenticated traffic.
Code

api/ratelimit/ratelimit.go[R98-107]

+func realIP(r *http.Request) string {
+	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+		if i := strings.IndexByte(xff, ','); i > 0 {
+			return strings.TrimSpace(xff[:i])
+		}
+		return strings.TrimSpace(xff)
+	}
+	if xrip := r.Header.Get("X-Real-Ip"); xrip != "" {
+		return strings.TrimSpace(xrip)
+	}
Evidence
realIP prefers forwarded headers over RemoteAddr without validating the request came from a trusted
proxy, so a direct client can set these headers to arbitrary values.

api/ratelimit/ratelimit.go[98-113]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Rate limiting can be bypassed because `realIP` trusts `X-Forwarded-For`/`X-Real-Ip` from any client.
### Issue Context
Without trusted proxy validation, direct clients can spoof these headers.
### Fix Focus Areas
- api/ratelimit/ratelimit.go[98-113]
- api/serve.go[126-134]
### Suggested fix
- Implement trusted-proxy validation:
- parse `RemoteAddr` IP
- only use `X-Forwarded-For`/`X-Real-Ip` if `RemoteAddr` is in a configured allowlist/CIDR set
- Add config/env to define trusted proxies and document deployment expectations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread pkgs/database/query_block.go
Comment thread pkgs/database/query_block.go
Comment thread pkgs/database/query_block.go
Comment thread pkgs/database/query_view.go
Comment thread pkgs/database/key_mgmt.go Outdated
Comment thread api/ratelimit/ratelimit.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
indexer/decoder/decoder.go (1)

224-235: ⚠️ Potential issue | 🔴 Critical

Bug: extractCoins produces duplicate entries due to pre-allocation + append.

The slice is pre-allocated with len(amount) zero-valued elements, then append adds more elements. This results in a slice with 2*len(amount) entries: the first half are zero-valued Coin{} structs.

🐛 Fix: Use indexed assignment or zero-length slice

Option 1: Use indexed assignment (preferred)

 func extractCoins(amount std.Coins) ([]Coin, error) {
 	coins := make([]Coin, len(amount))
-	for _, coin := range amount {
-		coins = append(coins, Coin{
+	for i, coin := range amount {
+		coins[i] = Coin{
 			Amount: coin.Amount,
 			Denom:  coin.Denom,
-		})
+		}
 	}
 	return coins, nil
 }

Option 2: Pre-allocate capacity only

 func extractCoins(amount std.Coins) ([]Coin, error) {
-	coins := make([]Coin, len(amount))
+	coins := make([]Coin, 0, len(amount))
 	for _, coin := range amount {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/decoder/decoder.go` around lines 224 - 235, The extractCoins function
pre-allocates a slice with make([]Coin, len(amount)) then uses append, causing
duplicate entries; fix by either replacing append with indexed assignment into
the pre-sized slice (coins[i] = Coin{...}) inside the loop over amount, or
change the allocation to a zero-length slice with capacity (make([]Coin, 0,
len(amount))) and keep append; update the loop in extractCoins and ensure Coin
and std.Coins usage remains consistent.
compression/train/process.go (1)

99-132: ⚠️ Potential issue | 🟡 Minor

processEvents silently skips marshal errors but never returns an error.

The function signature returns error but always returns nil. Marshal failures are logged and skipped. Consider either:

  1. Removing the error return if errors are intentionally non-fatal, or
  2. Returning an error (or aggregated errors) if any marshaling fails.

Current behavior may hide data loss issues silently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compression/train/process.go` around lines 99 - 132, The function
processEvents currently swallows proto.Marshal errors and always returns nil
error; update it so marshal failures are propagated: either remove the error
return from processEvents if you decide failures are non-fatal, or (recommended)
keep the signature and return an error when proto.Marshal fails (e.g., capture
the marshal error from proto.Marshal and return it immediately or aggregate
multiple errors and return a combined error after the loop). Locate
processEvents and the proto.Marshal call and modify the control flow to
propagate the error (include the underlying err in the returned error) instead
of only logging and continuing.
indexer/db_init/tables.go (1)

574-594: ⚠️ Potential issue | 🟠 Major

Quote or validate identifiers before concatenating these GRANT statements.

userName and each tableName are inserted verbatim into SQL. Names with punctuation or quotes will break setup, and untrusted values can inject additional SQL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/db_init/tables.go` around lines 574 - 594, The AppointPrivileges
function builds GRANT statements by concatenating userName and tableName
directly into the SQL, which risks broken identifiers and SQL injection; update
AppointPrivileges to validate the privilege value as before, but instead of
inserting raw identifiers use a proper identifier-quoting helper (e.g.,
QuoteIdentifier from your SQL driver or a small utility that wraps identifiers
in double quotes and escapes embedded quotes) for both userName and each
tableName when appending to the strings.Builder, and avoid interpolating
untrusted input into the SQL; ensure the "keymgr" branch also quotes "api_keys"
consistently and keep the same GRANT verbs.
🟠 Major comments (15)
indexer/address_cache/address.go-108-110 (1)

108-110: ⚠️ Potential issue | 🟠 Major

Missing error logging and potential silent failure.

When FindExistingAccounts fails, the error is silently swallowed and nil is returned. This causes insertWithRetry and fetchAndCacheInserted to no-op, effectively losing track of addresses that should be inserted. Consider logging the error and returning the original addresses slice to allow the insert attempt to proceed.

🐛 Proposed fix
 	existing, err := a.db.FindExistingAccounts(ctx, addresses, chainName, insertValidators)
 	if err != nil {
-		return nil
+		l.Error().Caller().Stack().Err(err).Msg("error finding existing accounts, proceeding with insert attempt")
+		return addresses
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/address_cache/address.go` around lines 108 - 110, The call to
FindExistingAccounts currently swallows errors and returns nil; change this so
errors are logged and the original addresses slice is returned to allow
downstream insertWithRetry and fetchAndCacheInserted to proceed. Locate the
block that checks "if err != nil" after calling FindExistingAccounts in the
function in address.go, replace the silent return with a process (or logger)
error call that includes the error and then return the original addresses slice
(not nil) so inserts are attempted; ensure the log message references
FindExistingAccounts and the surrounding context (e.g., function handling
addresses) for easier debugging.
indexer/data_processor/operator.go-194-229 (1)

194-229: ⚠️ Potential issue | 🟠 Major

Failed blocks leave zero-valued entries in the result slice.

Unlike processTransaction which uses a valid flag to filter failed entries, processBlock returns early on errors but leaves zero-valued sqlDataTypes.Blocks structs in blocksData. These will be inserted into the database, potentially causing integrity issues (e.g., height=0, empty hash).

🔧 Suggested fix: Add validity tracking like processTransaction
 func (d *DataProcessor) ProcessBlocks(blocks []*rpcClient.BlockResponse, fromHeight uint64, toHeight uint64) {
 	blockAmount := len(blocks)
 	blocksData := make([]sqlDataTypes.Blocks, blockAmount)
+	valid := make([]bool, blockAmount)
 	wg := sync.WaitGroup{}
 	wg.Add(blockAmount)

 	for idx, block := range blocks {
-		go d.processBlock(idx, block, &wg, blocksData)
+		go d.processBlock(idx, block, &wg, &valid[idx], blocksData)
 	}

 	wg.Wait()

+	// Collect only successfully processed blocks
+	result := make([]sqlDataTypes.Blocks, 0, blockAmount)
+	for idx, ok := range valid {
+		if ok {
+			result = append(result, blocksData[idx])
+		}
+	}
+
 	timeout := 10*time.Second + (time.Duration(blockAmount) * time.Second / 5)
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
-	err := d.dbPool.InsertBlocks(ctx, blocksData)
+	err := d.dbPool.InsertBlocks(ctx, result)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/data_processor/operator.go` around lines 194 - 229, processBlock
currently returns early on decode/parse errors and leaves zero-valued entries in
blocksData; fix by adding validity tracking like processTransaction: extend
processBlock signature to accept a valid []bool (or a pointer to a bool slice)
and set valid[idx] = true only after successfully populating blocksData[idx]; on
any early return leave valid[idx] false so the caller can filter out invalid
entries before database insertion. Ensure you update the caller to pass the same
valid slice and to filter blocksData by valid flags like processTransaction
does.
docker-compose-dev.yml-70-72 (1)

70-72: ⚠️ Potential issue | 🟠 Major

Use health-based depends_on to wait for service readiness.

The api service at lines 70-72 uses simple depends_on format that only ensures startup order. Since timescaledb and valkey have healthchecks defined, update to health-based conditions to ensure the API doesn't start until both services are healthy:

depends_on:
  timescaledb:
    condition: service_healthy
  valkey:
    condition: service_healthy

The indexer service (lines 42-43) should similarly be updated to reference the timescaledb healthcheck condition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose-dev.yml` around lines 70 - 72, Update the docker-compose
depends_on for the api and indexer services to use health-based conditions so
they wait for dependent services to be healthy: replace the simple list-style
depends_on under the api service (currently referencing timescaledb and valkey)
with a map that sets timescaledb: condition: service_healthy and valkey:
condition: service_healthy, and similarly change the indexer service's
depends_on (currently referencing timescaledb) to a map with timescaledb:
condition: service_healthy so both services will wait for the defined
healthchecks to pass before starting.
docker-compose.yml-56-58 (1)

56-58: ⚠️ Potential issue | 🟠 Major

Use health-gated depends_on for API startup reliability.

The current depends_on configuration (simple list format) only controls container start order—it does not wait for services to be healthy. Both timescaledb and valkey define healthchecks, so the API can start and attempt connections before these services are ready, causing race conditions and failures.

Convert to health-gated conditions:

🛠️ Proposed fix
   api:
     depends_on:
-      - timescaledb
-      - valkey
+      timescaledb:
+        condition: service_healthy
+      valkey:
+        condition: service_healthy

Apply this change to both docker-compose.yml (lines 56-58) and docker-compose-dev.yml (lines 70-72).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker-compose.yml` around lines 56 - 58, Replace the simple list-style
depends_on with health-gated depends_on for the API service so it waits for the
services' healthchecks; specifically change the depends_on block that currently
lists "timescaledb" and "valkey" to use objects with health conditions (e.g.,
depends_on: timescaledb: condition: service_healthy and valkey: condition:
service_healthy) and apply the same replacement in both docker-compose.yml and
docker-compose-dev.yml so the API will only start after timescaledb and valkey
report healthy.
api/valkey/client.go-37-43 (1)

37-43: 🛠️ Refactor suggestion | 🟠 Major

Context should be the first parameter per Go conventions.

Go idiom places context.Context as the first parameter. The current signature (key string, ctx context.Context) is non-idiomatic and inconsistent with standard library patterns.

♻️ Suggested fix
-func (c *ValkeyClient) Increment(key string, ctx context.Context) (int64, error) {
+func (c *ValkeyClient) Increment(ctx context.Context, key string) (int64, error) {
 	return c.client.Incr(ctx, key)
 }
 
-func (c *ValkeyClient) Expirer(key string, ctx context.Context, expiration time.Duration) (bool, error) {
+func (c *ValkeyClient) Expirer(ctx context.Context, key string, expiration time.Duration) (bool, error) {
 	return c.client.Expire(ctx, key, expiration)
 }

Note: This will require updating callers in api/ratelimit/ratelimit.go and any other consumers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/valkey/client.go` around lines 37 - 43, The method parameter order is
non-idiomatic: move context.Context to be the first parameter on ValkeyClient
methods by changing Increment(key string, ctx context.Context) to Increment(ctx
context.Context, key string) and Expirer(key string, ctx context.Context,
expiration time.Duration) to Expirer(ctx context.Context, key string, expiration
time.Duration); update all callers (e.g., usages in api/ratelimit/ratelimit.go
and any other consumers) to pass the context first and run a build to catch
remaining call sites.
go.mod-65-73 (1)

65-73: ⚠️ Potential issue | 🟠 Major

Update OpenTelemetry exporter versions to v1.42.0 to match the SDK.

OpenTelemetry-Go requires all stable (v1.x) modules to share the exact same version number. The current configuration mixes otel/sdk v1.42.0 with exporters at v1.40.0, which is an unsupported combination per the project's versioning policy. Update all exporter packages to v1.42.0:

  • go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
  • go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
  • go.opentelemetry.io/otel/exporters/otlp/otlptrace
  • go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go.mod` around lines 65 - 73, The go.mod currently mixes OpenTelemetry
modules at v1.40.0 with SDK modules at v1.42.0; update the exporter module
versions to match the SDK by changing the versions for
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc,
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp,
go.opentelemetry.io/otel/exporters/otlp/otlptrace, and
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp to v1.42.0 so
all stable v1.x otel modules use the identical version, then run the appropriate
go tooling (e.g., go get or go mod tidy) to update the lockfile.
api/handlers/address.go-32-35 (1)

32-35: ⚠️ Potential issue | 🟠 Major

Return 5xx for non-not-found failures.

Any DB timeout or connection error from GetDailyActiveAccount is currently rewritten to 404, which makes outages indistinguishable from missing data. Reserve 404 for an explicit no-data condition and surface the rest as 5xx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/handlers/address.go` around lines 32 - 35, The handler currently maps any
error from h.db.GetDailyActiveAccount to a 404; change this to only return
huma.Error404NotFound when the DB error explicitly indicates no-data (e.g.,
compare err to sql.ErrNoRows or your repository's NotFound sentinel/error type),
and for all other errors return a 5xx error (e.g.,
huma.Error500InternalServerError) including the original err; update the error
branch around the GetDailyActiveAccount call to perform this conditional check
and return the appropriate huma error while preserving the original error
details.
api/keystore/keystore.go-12-23 (1)

12-23: ⚠️ Potential issue | 🟠 Major

Hide the backing store behind a one-method interface.

With db fixed to *database.TimescaleDb, the fake in api/keystore/keystore_test.go cannot be injected, so the new tests have to copy Refresh behavior by hand instead of exercising KeyStore.Refresh / KeyStore.StartPeriodicRefresh. That leaves regressions in the real refresh path effectively untested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/keystore/keystore.go` around lines 12 - 23, The KeyStore currently
hardcodes db as *database.TimescaleDb which prevents injecting a fake in tests;
define a minimal one-method interface (e.g., KeyStoreDB with the single method
used by KeyStore.Refresh), change KeyStore.db to that interface type and update
NewKeyStore signature to accept the interface, implement an adapter that wraps
*database.TimescaleDb for production, and update api/keystore/keystore_test.go
to inject a fake implementing that interface so tests exercise KeyStore.Refresh
and KeyStore.StartPeriodicRefresh directly.
api/key_cmd.go-88-91 (1)

88-91: ⚠️ Potential issue | 🟠 Major

Don't silently fall back to 5432 when KEY_DB_PORT is malformed.

If the env var is set to a non-number, this path ignores the parse failure and later uses the default port. That can point key-management commands at the wrong database instead of surfacing the config error.

Proposed fix
+import "strconv"
+
 ...
 	if params.port == 0 {
 		if v := os.Getenv("KEY_DB_PORT"); v != "" {
-			fmt.Sscanf(v, "%d", &params.port)
+			port, err := strconv.Atoi(v)
+			if err != nil {
+				return nil, fmt.Errorf("invalid KEY_DB_PORT %q: %w", v, err)
+			}
+			params.port = port
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/key_cmd.go` around lines 88 - 91, The code currently ignores parse errors
when reading KEY_DB_PORT into params.port, causing a silent fallback to the
default 5432; change the parsing so a malformed KEY_DB_PORT surfaces an error
(don’t let params.port silently remain 0). Replace the fmt.Sscanf call with
strconv.Atoi (or use fmt.Sscan with error handling) to capture parse errors, and
if parsing fails return or propagate an explicit error from the command
initialization (or log and exit) so callers know the env var was invalid;
reference the params.port variable and the KEY_DB_PORT env var when implementing
the check.
pkgs/database/key_mgmt.go-155-168 (1)

155-168: ⚠️ Potential issue | 🟠 Major

Inconsistent hash parameter format.

EnableKey passes hash (the [32]byte array) directly to the query at line 160, while DisableKey (line 145) and AdjustRpmLimit (line 177) pass hash[:] (a slice). This inconsistency may cause unexpected behavior depending on how pgx handles the parameter.

🐛 Proposed fix
 func (t *TimescaleDb) EnableKey(ctx context.Context, hash [32]byte) error {
 	result, err := t.pool.Exec(ctx, `
 		UPDATE api_keys
 		SET is_active = true
 		WHERE hash = $1
-		`, hash)
+		`, hash[:])
 	if err != nil {
 		return err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/key_mgmt.go` around lines 155 - 168, EnableKey currently passes
the [32]byte array value directly to t.pool.Exec while DisableKey and
AdjustRpmLimit use hash[:] (a byte slice), causing inconsistent parameter types;
change the Exec call in EnableKey (function EnableKey) to pass hash[:] instead
of hash so the query parameter type matches the other functions and avoids pgx
type-mismatch behavior, keep the rest of the update/rows-affected handling
unchanged.
pkgs/database/query_block.go-138-145 (1)

138-145: ⚠️ Potential issue | 🟠 Major

Transaction query does not align with the blocks being fetched.

query2 fetches an arbitrary LIMIT $2 of transactions without filtering by the block heights returned in query1. This means transactions may be fetched for blocks outside the "last X blocks" range.

Consider using a subquery or filtering by block_height >= (SELECT MIN(height) FROM ...) to ensure transactions correspond to the returned blocks.

🐛 Proposed fix
 	query2 := `
 	SELECT 
 	encode(tx_hash, 'base64'),
 	block_height
 	FROM transaction_general
 	WHERE chain_name = $1
-	LIMIT $2
+	AND block_height IN (
+		SELECT height FROM blocks
+		WHERE chain_name = $1
+		ORDER BY height DESC
+		LIMIT $2
+	)
 	`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/query_block.go` around lines 138 - 145, query2 currently
selects an arbitrary LIMIT $2 of transactions and can return txs from blocks
outside the "last X blocks" returned by query1; update the transaction query
(query2) to restrict results to the block heights produced by query1 by adding a
filter on transaction_general.block_height (for example using a subquery or IN
clause that selects heights from the same source used in query1 or by requiring
block_height >= (SELECT MIN(height) FROM ... ) ), ensure you keep the
chain_name/$1 filter and the LIMIT/$2 parameter, and reference the
transaction_general table and columns tx_hash and block_height when making this
change.
pkgs/database/query_view.go-55-64 (1)

55-64: ⚠️ Potential issue | 🟠 Major

Coalesce empty aggregates to zero.

SUM(...) returns NULL when the window has no rows. Scanning that into int64 turns “no data in this window” into an error/404 for these count endpoints, when they should return 0 instead.

Possible fix
- SUM(block_count) as block_count
+ COALESCE(SUM(block_count), 0) as block_count

- SUM(transaction_count) as total_tx_count
+ COALESCE(SUM(transaction_count), 0) as total_tx_count

- SUM(transaction_count) as tx_count_24h
+ COALESCE(SUM(transaction_count), 0) as tx_count_24h

Also applies to: 111-120, 131-141

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/query_view.go` around lines 55 - 64, The SUM(...) aggregate in
the SQL query can return NULL for empty windows which causes row.Scan into int64
(variable blockCount) to fail; update the SQL to wrap the aggregate with
COALESCE, e.g. COALESCE(SUM(block_count), 0) in the query string used before the
call to t.pool.QueryRow(ctx, query, chainName) so row.Scan(&blockCount) always
receives 0 instead of NULL; apply the same COALESCE fix to the other aggregate
queries referenced (the blocks around lines 111-120 and 131-141) so their
scanned int64/count variables never get NULL.
api/handlers/transactions.go-215-217 (1)

215-217: ⚠️ Potential issue | 🟠 Major

Don't surface storage failures as 400s.

At this point the request has already been validated and the transaction has been found. If one of these follow-up fetches fails, that's a missing-data/backend problem, not a bad request. Returning 400 here will misclassify valid requests and break retry/monitoring behavior. Please reserve 400 for malformed inputs, use 404 only when the message payload is genuinely absent, and return a 5xx for query failures.

Also applies to: 246-248, 277-279, 308-310

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/handlers/transactions.go` around lines 215 - 217, The handler is
returning huma.Error400BadRequest for storage/query failures (e.g., the
fmt.Sprintf call that mentions "vm_msg_call" and txHash and the similar blocks
at the other locations), but these are backend/missing-data issues; change the
logic so that when the follow-up fetch fails you distinguish between "not found"
(return a 404 Not Found) and other storage/query errors (return a 5xx, e.g.,
huma.Error500InternalServerError) and include the original error in the
log/response context; update the three similar sites referenced (the block with
"vm_msg_call" and the other blocks at the cited ranges) to use this pattern.
indexer/cmd/setup.go-108-113 (1)

108-113: ⚠️ Potential issue | 🟠 Major

Make setup create-db resumable.

If anything after CreateDatabase fails, rerunning against postgres will fail on the existing DB, while rerunning against the target DB takes the currentDb == newDbName branch and skips the remaining setup entirely. Please split “create DB if missing” from “initialize schema” so the command can safely resume after a partial failure.

Also applies to: 130-164

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/cmd/setup.go` around lines 108 - 113, The current logic returns early
when currentDb == newDbName which skips schema initialization on retry; change
it to split DB creation from schema initialization by first ensuring the
database exists (call or extract CreateDatabase into a helper like
ensureDatabaseExists or reuse the existing CreateDatabase call against the
postgres connection when currentDb != newDbName) and then always call
initializeNewDatabase (or the schema init function) against the target DB
regardless of whether creation was needed; update the branch that uses
currentDb/newDbName so it only skips CreateDatabase but still invokes
initializeNewDatabase (apply the same split to the other similar block
referenced in lines 130-164).
pkgs/database/query_view.go-325-339 (1)

325-339: ⚠️ Potential issue | 🟠 Major

This 24h rollup drops buckets where the validator missed every block.

Starting from validator_signing_counter means any hour with zero signed blocks has no vsc row, so it never participates in the join. That undercounts total_blocks, underreports missed blocks, and inflates signing_rate_pct. Drive this aggregation from block_counter and left join the validator counts instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/query_view.go` around lines 325 - 339, The current SQL
(assigned to query2) incorrectly drives the 24h rollup from
validator_signing_counter (vsc), which omits hours with zero signed blocks;
rewrite the query to aggregate from block_counter (bc) as the primary table and
LEFT JOIN validator_signing_counter vsc ON bc.time_bucket = vsc.time_bucket AND
bc.chain_name = vsc.chain_name AND vsc.validator_id = $2, then filter by
bc.chain_name = $1 and bc.time_bucket in the 24-hour window; recompute
blocks_signed using coalesce(sum(vsc.blocks_signed),0), blocks_not_signed as
coalesce(sum(bc.block_count),0) - coalesce(sum(vsc.blocks_signed),0),
total_blocks from bc, and signing_rate_pct using the same nullif-safe division
so missing vsc rows do not drop buckets.
🟡 Minor comments (5)
api/handlers/address.go-25-26 (1)

25-26: ⚠️ Potential issue | 🟡 Minor

Allow a single-day window.

This check rejects start_date == end_date, so clients can't request exactly one day's daily-active data.

Proposed fix
-	if !input.StartDate.Before(input.EndDate) {
-		return nil, huma.Error400BadRequest("start_date must be before end_date", nil)
+	if input.StartDate.After(input.EndDate) {
+		return nil, huma.Error400BadRequest("start_date must be on or before end_date", nil)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/handlers/address.go` around lines 25 - 26, The current validation rejects
equal start and end dates by using if !input.StartDate.Before(input.EndDate) —
allow single-day windows by changing the check to only fail when start is after
end (use input.StartDate.After(input.EndDate)); update the error path that
returns huma.Error400BadRequest("start_date must be before end_date", nil) to
remain but ensure it only triggers on After, preserving the existing message or
adjust to "start_date must be on or before end_date" if you prefer clearer
wording; locate and modify the conditional that references input.StartDate and
input.EndDate in the handler function where this validation occurs.
pkgs/database/types_api.go-172-173 (1)

172-173: ⚠️ Potential issue | 🟡 Minor

omitempty needs to live inside the json tag.

encoding/json only recognizes options attached to the json key in comma-separated format, so the current tag still emits "time":null for a nil Time instead of omitting it.

Proposed fix
 type ValidatorSigning struct {
-	Time         *time.Time `json:"time" doc:"Time" omitempty:"true"`
+	Time         *time.Time `json:"time,omitempty" doc:"Time"`
 	BlocksSigned int64      `json:"blocks_signed" doc:"Blocks signed"`
 	BlocksMissed int64      `json:"blocks_missed" doc:"Blocks missed"`
 	TotalBlocks  int64      `json:"blocks_total" doc:"Total blocks"`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/types_api.go` around lines 172 - 173, The struct field
ValidatorSigning.Time uses omitempty as a separate tag key which encoding/json
ignores; update the struct tag on ValidatorSigning.Time so the json key includes
the omitempty option (e.g., change the json tag to include ",omitempty") and
remove the standalone omitempty:"true" tag while preserving the doc tag; locate
ValidatorSigning and update the Time field's struct tag accordingly.
api/serve.go-135-137 (1)

135-137: ⚠️ Potential issue | 🟡 Minor

Minor typo in API documentation.

"APi" should be "API" in the security scheme description.

📝 Proposed fix
 		Description: `API key for authenticated access with a higher rate limit. Pass your key in the X-API-Key header.
-			You can query the APi without the API key but the stricter rate limit will be applied.`,
+			You can query the API without the API key but the stricter rate limit will be applied.`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/serve.go` around lines 135 - 137, The Description string for the security
scheme in api/serve.go contains a typo ("APi") — update the Description value
(the Description field inside the security scheme definition) to use "API"
instead of "APi" so the text reads "You can query the API without the API key
but the stricter rate limit will be applied."; keep the rest of the string
unchanged.
api/serve.go-171-179 (1)

171-179: ⚠️ Potential issue | 🟡 Minor

Error handling after partial write is ineffective.

If w.Write(favicon) partially succeeds (writes some bytes but returns an error), calling http.Error will fail silently because headers have already been sent. The error is logged, which is good, but http.Error won't change the response status at that point.

Consider restructuring to avoid misleading error handling:

🛡️ Proposed fix
 	router.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Content-Type", "image/x-icon")
 		_, err := w.Write(favicon)
 		if err != nil {
 			log.Printf("failed to write favicon: %v", err)
-			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
-			return
+			// Headers already sent; cannot change response status
 		}
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/serve.go` around lines 171 - 179, The handler for "/favicon.ico"
currently calls http.Error after a write that may have partially succeeded,
which is ineffective once headers/body have been sent; change the handler in the
router.Get("/favicon.ico", ...) block to first write the favicon into an
in-memory buffer (e.g., bytes.Buffer or similar), set the Content-Length header
from the buffer size, then write the buffer to w; if w.Write returns an error,
only log the error (do not call http.Error) because the response may already be
partially sent. Ensure you update the handler code paths around the Write of
favicon to follow this buffering-and-log-only approach.
api/handlers/blocks.go-47-49 (1)

47-49: ⚠️ Potential issue | 🟡 Minor

Inconsistent error message.

The condition input.ToHeight-input.FromHeight > 100 allows up to 101 blocks (from=1, to=101), but the error message says "difference must be less than 100". Consider updating the message to "must be at most 100" or changing the condition to >= 100 if you want a maximum of 100 blocks.

📝 Proposed fix (if 100 blocks max is intended)
-	if input.ToHeight-input.FromHeight > 100 {
-		return nil, huma.Error400BadRequest("From height and to height difference must be less than 100", nil)
+	if input.ToHeight-input.FromHeight >= 100 {
+		return nil, huma.Error400BadRequest("Range must not exceed 100 blocks", nil)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/handlers/blocks.go` around lines 47 - 49, The check on input.ToHeight and
input.FromHeight is inconsistent with its message: change the condition from
`input.ToHeight-input.FromHeight > 100` to `input.ToHeight-input.FromHeight >=
100` and update the huma.Error400BadRequest message to "From height and to
height difference must be at most 100" (adjust the string passed to
huma.Error400BadRequest where the current check lives).
🧹 Nitpick comments (15)
indexer/data_processor/operator.go (2)

498-503: Consider moving slice assignment outside the critical section.

The decodedMsgs[idx] assignment doesn't require mutex protection since each goroutine writes to its own pre-allocated index. Moving it outside the lock reduces contention:

♻️ Suggested optimization
 	mu.Lock()
 	for _, address := range addresses {
 		(*addressesMap)[address] = struct{}{}
 	}
-	decodedMsgs[idx] = decodedMsg
 	mu.Unlock()
+	decodedMsgs[idx] = decodedMsg
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/data_processor/operator.go` around lines 498 - 503, The critical
section currently holds mu while adding addresses to addressesMap and also while
writing decodedMsgs[idx]; move only the addressesMap updates inside the
mu.Lock()/mu.Unlock() and perform the decodedMsgs[idx] = decodedMsg assignment
outside the lock since each goroutine writes to its own pre-allocated index (use
the existing variables mu, addressesMap, addresses, decodedMsg, and idx to
identify the code to change).

406-413: Redundant bounds check - idx from range will never exceed slice length.

The guard if idx >= transactionAmount is unreachable since idx comes from iterating range transactions and transactionAmount = len(transactions). This check can be removed to reduce noise.

♻️ Simplified loop
 	for idx, transaction := range transactions {
-		// Guard against index out of bounds
-		if idx >= transactionAmount {
-			wg.Done()
-			continue
-		}
 		go d.processMessageGroup(idx, transaction, allDecodedMsgs[idx], &wg, &msgResults)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/data_processor/operator.go` around lines 406 - 413, The bounds check
"if idx >= transactionAmount { wg.Done(); continue }" is redundant because idx
comes from ranging over transactions and transactionAmount == len(transactions);
remove that entire if-block so the loop simply ranges and launches goroutines
with go d.processMessageGroup(idx, transaction, allDecodedMsgs[idx], &wg,
&msgResults). Ensure wg.Add was called appropriately before this loop and that
processMessageGroup is responsible for calling wg.Done() so the waitgroup
accounting remains correct.
api/config/loader.go (1)

90-102: Consider extracting shared env-loading helper.

LoadValkeyEnvironment repeats the same read+parse flow used by LoadEnvironment. A shared helper would reduce duplication and keep error behavior consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/config/loader.go` around lines 90 - 102, Extract the duplicated
read+parse logic into a shared helper (e.g., a function like loadEnvInto(reader
EnvFileReader, path string, out interface{}) error) and use it from both
LoadValkeyEnvironment and LoadEnvironment; the helper should call
reader.ReadFile(path) and then env.Parse(out), returning wrapped errors
consistently, and the existing functions should simply create their specific
struct (ValkeyEnv or the other env struct), call the helper to populate it, and
return the pointer or error.
api/key/generate.go (1)

10-17: Extract key-format constants to avoid drift.

Lines 10 and 17 use magic values (32, 10) tied to key format. A small constants block would make future format changes safer.

♻️ Proposed refactor
 func GenerateApiKey() (string, string, [32]byte, error) {
-	rawKey := make([]byte, 32)
+	const (
+		apiKeyByteLen = 32
+		apiKeyPrefix  = "api_"
+		keyPrefixLen  = 10
+	)
+	rawKey := make([]byte, apiKeyByteLen)
 	_, err := rand.Read(rawKey)
 	if err != nil {
 		return "", "", [32]byte{}, err
 	}
-	rawKeyHex := "api_" + hex.EncodeToString(rawKey)
+	rawKeyHex := apiKeyPrefix + hex.EncodeToString(rawKey)
 	keyHash := sha256.Sum256([]byte(rawKeyHex))
-	keyPrefix := rawKeyHex[:10]
+	keyPrefix := rawKeyHex[:keyPrefixLen]
 	return rawKeyHex, keyPrefix, keyHash, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/key/generate.go` around lines 10 - 17, Replace the magic numbers used
when generating the key by introducing named constants (e.g., apiKeyRandomBytes
= 32, apiKeyPrefix = "api_", apiKeyPrefixLen = 10) and use them when creating
rawKey, building rawKeyHex and slicing keyPrefix; update the uses in the rawKey,
rawKeyHex and keyPrefix expressions (and keep keyHash calculation as-is) so
future format changes only require updating the constants.
CHANGELOG.md (2)

29-29: Section header should be "Changed" for consistency.

The Keep a Changelog format uses "Changed" (see line 45, 60, 97). This section uses "Changes" which is inconsistent.

📝 Suggested fix
-### Changes
+### Changed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 29, Replace the inconsistent section header "###
Changes" with the canonical Keep a Changelog header "### Changed" so it matches
other sections (e.g., lines showing "### Changed"); update the header string
used in the CHANGELOG.md to "### Changed" wherever this instance appears.

12-16: Minor grammar improvements recommended.

A few grammar/style refinements per static analysis:

📝 Suggested fixes
-The zstd compression has been added. This it is not production ready and still in development, however
+The zstd compression has been added. This is not production-ready and still in development, however
 initial testing has been done and it seems to work, about 30% less storage is required.
 
-The API has been improved with some new routes and minor improvements. The cursor based pagination has been added
+The API has been improved with some new routes and minor improvements. The cursor-based pagination has been added
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 12 - 16, Fix small grammar and punctuation issues
in the changelog: in the zstd sentence change "This it is not production ready
and still in development, however" to "It is not production-ready and is still
in development" and rephrase "about 30% less storage is required." to "it
requires about 30% less storage."; in the API paragraph hyphenate "cursor-based
pagination" and change "The API now also has POST utilities to convert between
Base64 and Base64URL." to "The API now includes POST utilities for converting
between Base64 and Base64URL." Update these exact phrases in CHANGELOG.md so
wording, hyphenation, and sentence flow are corrected.
api/valkey/client.go (1)

15-31: Consider making timeout configurable.

The 5-second timeout is hardcoded. For production flexibility, consider accepting timeout as a parameter or via configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/valkey/client.go` around lines 15 - 31, The NewValkeyClient function
currently hardcodes timeout := 5 * time.Second; make the timeout configurable by
adding a timeout parameter (or reading from a config) to NewValkeyClient and use
that value when calling cfg.WithConnectionTimeout and cfg.WithRequestTimeout;
update the function signature (e.g., NewValkeyClient(host string, port int,
timeout time.Duration) or accept an options/config struct) and ensure
ValkeyClient initialization and any callers are adjusted to pass the desired
timeout rather than the fixed 5s.
compression/train/process.go (1)

77-84: Consider making debug output configurable.

DebugOut: os.Stdout will write debug information to stdout unconditionally. For production use, consider making this configurable or using a proper logger.

♻️ Suggested approach
-func BuildZstdDict(events [][]byte) ([]byte, error) {
+func BuildZstdDict(events [][]byte, debugWriter io.Writer) ([]byte, error) {
 	const maxHistorySize = 112 << 10
 	// ...
 	dict, err := zstd.BuildDict(zstd.BuildDictOptions{
 		ID:       1,
 		Contents: events,
 		History:  history,
 		Level:    lvl,
-		DebugOut: os.Stdout,
+		DebugOut: debugWriter, // pass nil to disable, or os.Stdout for CLI usage
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compression/train/process.go` around lines 77 - 84, The BuildDictOptions
currently sets DebugOut: os.Stdout which always writes debug info; change it to
be configurable by exposing a logger/output option (or reading an env/config
flag) and pass that value into zstd.BuildDictOptions.DebugOut instead of
os.Stdout, or set DebugOut to nil when debug is disabled; update the code around
zstd.BuildDict and the variable (e.g., lvl, dict, err in process.go) to obtain
the output stream from the new config (or a logger wrapper) and use it in
BuildDictOptions so debug output can be toggled or redirected in production.
api/ratelimit/ratelimit_test.go (1)

123-136: Simplify hex encoding using standard library.

The inline hex encoding function can be replaced with encoding/hex:

♻️ Suggested simplification
+import "encoding/hex"
+
 // ensure key-based identifier used (first 8 bytes of hash)
 short := (h[:8])
-id := "key:" + (func() string { return (func(b []byte) string {
-	const hextable = "0123456789abcdef"
-	out := make([]byte, len(b)*2)
-	for i, v := range b {
-		out[i*2] = hextable[v>>4]
-		out[i*2+1] = hextable[v&0x0f]
-	}
-	return string(out)
-})(short) })()
+id := "key:" + hex.EncodeToString(short)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/ratelimit/ratelimit_test.go` around lines 123 - 136, Replace the custom
inline hex encoder used to build id from short (the first 8 bytes of h) with the
standard library: call encoding/hex's EncodeToString on short to produce the hex
string when constructing id (variable names: h, short, id) so the test becomes
simpler and more readable while keeping the subsequent check against
vk.expireCalls["rl:"+id] unchanged.
go.mod (1)

35-35: Dependency changed from indirect to direct without being in main require block.

github.com/cosmos/gogoproto v1.7.2 was changed from indirect to direct (missing // indirect comment) but appears in the secondary require block rather than the main one. If this is intentionally a direct dependency, consider moving it to the main require block (lines 5-25) for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@go.mod` at line 35, The go.mod entry for github.com/cosmos/gogoproto v1.7.2
was promoted from indirect to direct but was left in the secondary require
block; move the dependency line "github.com/cosmos/gogoproto v1.7.2" into the
primary require block (the top require {...} block) so it is clearly a direct
dependency, or if it should remain indirect add the "// indirect" comment after
the version; update the require placement and comment accordingly to keep go.mod
semantics consistent.
api/huma-types/address.go (1)

28-31: Consider aligning field names with query parameter names for clarity.

The struct field names (StartDate, EndDate) don't match the query parameter names (start_timestamp, end_timestamp). While functional, this inconsistency may cause confusion during maintenance.

💡 Suggested alignment

Either rename fields to match the query params:

 type DailyActiveAccountGetInput struct {
-	StartDate time.Time `query:"start_timestamp" doc:"Start date (inclusive)" format:"date-time" required:"true"`
-	EndDate   time.Time `query:"end_timestamp" doc:"End date (inclusive)" format:"date-time" required:"true"`
+	StartTimestamp time.Time `query:"start_timestamp" doc:"Start timestamp (inclusive)" format:"date-time" required:"true"`
+	EndTimestamp   time.Time `query:"end_timestamp" doc:"End timestamp (inclusive)" format:"date-time" required:"true"`
 }

Or update the query params to match the field semantics (if the intent is date-only):

 type DailyActiveAccountGetInput struct {
-	StartDate time.Time `query:"start_timestamp" doc:"Start date (inclusive)" format:"date-time" required:"true"`
-	EndDate   time.Time `query:"end_timestamp" doc:"End date (inclusive)" format:"date-time" required:"true"`
+	StartDate time.Time `query:"start_date" doc:"Start date (inclusive)" format:"date-time" required:"true"`
+	EndDate   time.Time `query:"end_date" doc:"End date (inclusive)" format:"date-time" required:"true"`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/huma-types/address.go` around lines 28 - 31, The struct
DailyActiveAccountGetInput uses field names StartDate and EndDate that don't
match the query tag names start_timestamp and end_timestamp; update one side for
clarity and maintainability—either rename the fields to StartTimestamp and
EndTimestamp to mirror the `query:"start_timestamp"`/`query:"end_timestamp"`
tags, or change the query tags to `query:"start_date"`/`query:"end_date"` (and
adjust any consumers) so the field names and query param names are consistent;
locate the struct in api/huma-types/address.go and update both the field
identifiers and any related docs or handlers referencing
DailyActiveAccountGetInput to keep behavior intact.
pkgs/sql_data_types/hypertables.go (1)

39-47: Consider extracting a shared helper for TableColumns() to reduce duplication.

The TableColumns() method is duplicated across all 8 structs with identical reflection logic. This violates DRY and increases maintenance burden.

♻️ Proposed refactor: Extract a generic helper

Add a package-level helper function:

// GetTableColumnsFromStruct extracts column names from struct tags
func GetTableColumnsFromStruct(v any) []string {
	columns := make([]string, 0)
	fields := reflect.TypeOf(v)
	for i := 0; i < fields.NumField(); i++ {
		field := fields.Field(i)
		columns = append(columns, field.Tag.Get("db"))
	}
	return columns
}

Then each struct's method becomes a one-liner:

 func (b Blocks) TableColumns() []string {
-	columns := make([]string, 0)
-	fields := reflect.TypeOf(b)
-	for i := 0; i < fields.NumField(); i++ {
-		field := fields.Field(i)
-		columns = append(columns, field.Tag.Get("db"))
-	}
-	return columns
+	return GetTableColumnsFromStruct(b)
 }

Also applies to: 76-85, 113-122, 164-173, 225-233, 295-304, 363-372, 422-432

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/sql_data_types/hypertables.go` around lines 39 - 47, The TableColumns()
reflection logic is duplicated across multiple structs (e.g.,
Blocks.TableColumns()), so extract a single package-level helper
GetTableColumnsFromStruct(v any) that contains the reflect.TypeOf loop and tag
extraction, then replace each struct method (like Blocks.TableColumns(), and the
other seven identical TableColumns methods) with a one-line wrapper that calls
GetTableColumnsFromStruct(b) to return the columns; this centralizes the logic
and removes duplication.
api/huma-types/validator.go (1)

9-11: Inconsistent validation constraints for ValidatorAddress.

ValidatorSigning24hGetInput.ValidatorAddress lacks minLength and maxLength constraints, while ValidatorSigningByHourGetInput.ValidatorAddress (line 18) has minLength:"40" maxLength:"40". Consider adding the same constraints for consistent API validation.

♻️ Proposed fix
 type ValidatorSigning24hGetInput struct {
-	ValidatorAddress string `path:"validator_address" doc:"Validator consensus address" required:"true"`
+	ValidatorAddress string `path:"validator_address" doc:"Validator consensus address" required:"true" minLength:"40" maxLength:"40"`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/huma-types/validator.go` around lines 9 - 11, The
ValidatorSigning24hGetInput struct's ValidatorAddress field is missing the
minLength and maxLength tags; update
ValidatorSigning24hGetInput.ValidatorAddress to include the same validation tags
used by ValidatorSigningByHourGetInput (minLength:"40" maxLength:"40") so both
inputs enforce identical 40-character address constraints in their struct field
tags.
api/handlers/validators.go (1)

30-32: Consider distinguishing "not found" from other database errors.

Both handlers return 404 for any database error. While this is a common pattern to avoid leaking internal details, it could make debugging difficult. A database connection failure would appear as "not found" to clients.

Consider logging the actual error internally (you're already passing it to huma.Error404NotFound) and potentially returning 500 for non-"not found" errors if the database layer can distinguish them.

Also applies to: 54-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/handlers/validators.go` around lines 30 - 32, The current handler always
returns huma.Error404NotFound for any DB error when looking up
input.ValidatorAddress; change the error handling so only a true "not found"
sentinel (e.g., sql.ErrNoRows or your DB layer's NotFound error) maps to
huma.Error404NotFound while all other errors are logged internally and return a
500-style error (e.g., huma.Error500InternalServerError); preserve the existing
log of the actual error, use the specific error check before calling
huma.Error404NotFound, and apply the same pattern to the other block referenced
at lines 54-57.
api/ratelimit/ratelimit.go (1)

99-104: Edge case in X-Forwarded-For parsing.

If X-Forwarded-For starts with a comma (malformed header like , 1.2.3.4), strings.IndexByte returns 0, which fails the > 0 check, causing the full malformed string to be returned. Consider using >= 0 or handling the edge case:

♻️ Proposed fix
 func realIP(r *http.Request) string {
 	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
-		if i := strings.IndexByte(xff, ','); i > 0 {
+		if i := strings.IndexByte(xff, ','); i >= 0 {
+			if i == 0 {
+				// Malformed: starts with comma, try next segment
+				parts := strings.SplitN(xff, ",", 3)
+				if len(parts) > 1 {
+					return strings.TrimSpace(parts[1])
+				}
+			}
 			return strings.TrimSpace(xff[:i])
 		}
 		return strings.TrimSpace(xff)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/ratelimit/ratelimit.go` around lines 99 - 104, The X-Forwarded-For
parsing block using r.Header.Get("X-Forwarded-For") and strings.IndexByte(xff,
',') doesn't handle headers that start with a comma (e.g., ", 1.2.3.4") because
the check i > 0 will fail and return the malformed string; change the logic to
robustly extract the first non-empty IP token by splitting the xff value on ','
(or scanning for the first token) and returning the first token after trimming
whitespace and leading commas; update the code around the strings.IndexByte(xff,
',') usage in this function to skip empty tokens and return a trimmed, non-empty
IP string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 630a5e19-3387-4b45-8d6c-82a01a8a3725

📥 Commits

Reviewing files that changed from the base of the PR and between 94ae1ed and e908881.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (58)
  • .env.example
  • CHANGELOG.md
  • Makefile
  • api/config/loader.go
  • api/config/types.go
  • api/handlers/address.go
  • api/handlers/blocks.go
  • api/handlers/blocks_test.go
  • api/handlers/database_test.go
  • api/handlers/interface.go
  • api/handlers/transactions.go
  • api/handlers/validators.go
  • api/huma-types/address.go
  • api/huma-types/block.go
  • api/huma-types/transaction.go
  • api/huma-types/validator.go
  • api/key/generate.go
  • api/key_cmd.go
  • api/keystore/keystore.go
  • api/keystore/keystore_test.go
  • api/main.go
  • api/ratelimit/ratelimit.go
  • api/ratelimit/ratelimit_test.go
  • api/routes/addresses.go
  • api/routes/blocks.go
  • api/routes/transactions.go
  • api/routes/utils.go
  • api/routes/validators.go
  • api/serve.go
  • api/valkey/client.go
  • compression/train/process.go
  • config-api.yml.example
  • docker-compose-dev.yml
  • docker-compose.yml
  • go.mod
  • indexer/address_cache/address.go
  • indexer/address_cache/address_test.go
  • indexer/cmd/live.go
  • indexer/cmd/setup.go
  • indexer/data_processor/operator.go
  • indexer/data_processor/types.go
  • indexer/db_init/hypertable.go
  • indexer/db_init/materialize.go
  • indexer/db_init/tables.go
  • indexer/decoder/decoder.go
  • indexer/decoder/product.go
  • indexer/decoder/types.go
  • indexer/orchestrator/operator.go
  • indexer/orchestrator/operator_test.go
  • indexer/orchestrator/types.go
  • pkgs/database/insert.go
  • pkgs/database/key_mgmt.go
  • pkgs/database/query_block.go
  • pkgs/database/query_view.go
  • pkgs/database/types_api.go
  • pkgs/sql_data_types/hypertables.go
  • pkgs/sql_data_types/materialize.go
  • pkgs/sql_data_types/table.go
💤 Files with no reviewable changes (2)
  • pkgs/database/insert.go
  • indexer/cmd/live.go

Comment thread indexer/address_cache/address.go
Comment thread indexer/address_cache/address.go
Comment thread indexer/db_init/hypertable.go
Comment thread pkgs/database/query_block.go
Comment thread pkgs/database/query_view.go
Comment thread pkgs/database/query_view.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

♻️ Duplicate comments (2)
indexer/db_init/hypertable.go (1)

73-80: ⚠️ Potential issue | 🔴 Critical

timescaledb.segmentby is still emitted with invalid SQL value formatting.

timescaledb.segmentby = %s injects raw text instead of a string literal, which can break ALTER TABLE ... SET (...) parsing at runtime. This was already flagged earlier and appears unresolved.

🐛 Proposed fix
-		columnsString := strings.Join(columns, ", ")
+		columnsString := strings.Join(columns, ",")

 		sql := fmt.Sprintf(
 			`
 			ALTER TABLE %s SET (
 				timescaledb.enable_columnstore,
-				timescaledb.segmentby = %s,
+				timescaledb.segmentby = '%s',
 				timescaledb.orderby = 'timestamp DESC'
 			);
 			`, tableName, columnsString)
#!/bin/bash
# Verify unresolved raw segmentby interpolation still exists.
rg -nP "timescaledb\.segmentby\s*=\s*%s" indexer/db_init/hypertable.go

# Expected after fix: no matches.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/db_init/hypertable.go` around lines 73 - 80, The ALTER TABLE SQL in
hypertable.go builds a string via fmt.Sprintf that injects columnsString into
the timescaledb.segmentby clause without quoting, causing invalid SQL; update
the fmt.Sprintf call that constructs sql (the ALTER TABLE ... SET (...) block)
to emit a properly quoted string literal for timescaledb.segmentby (e.g., wrap
the %s in single quotes or otherwise escape/quote columnsString) so the
generated ALTER TABLE uses timescaledb.segmentby = '<columns>' instead of
unquoted raw text.
indexer/address_cache/address.go (1)

148-155: ⚠️ Potential issue | 🔴 Critical

Context is cancelled before use, causing all inserts to fail.

Same issue as insertWithRetry - the context is cancelled immediately after creation (line 150) before being used in InsertAddresses (line 152).

🐛 Proposed fix: call cancel() after the operation
 	for _, addr := range addresses {
 		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-		cancel()
 
 		if err := a.db.InsertAddresses(ctx, []string{addr}, chainName, insertValidators); err != nil {
 			l.Error().Caller().Stack().Err(err).Msgf("error inserting address: %s", addr)
 		}
+		cancel()
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/address_cache/address.go` around lines 148 - 155, The context is
being cancelled immediately after creation which makes every call to
a.db.InsertAddresses fail; move the cancel() call so it runs after the
InsertAddresses call (and its error handling) instead of directly after
context.WithTimeout — create ctx, cancel := context.WithTimeout(...), call
a.db.InsertAddresses(ctx, ...), handle any error, then call cancel() (or ensure
cancel is invoked in a defer scoped to just that iteration) to properly release
the timeout context.
🧹 Nitpick comments (4)
indexer/data_processor/operator.go (3)

478-486: Move slice assignment outside the critical section.

decodedMsgs[idx] = decodedMsg doesn't need mutex protection since each goroutine writes to its own index slot. Including it inside the lock unnecessarily extends the critical section and increases contention.

♻️ Suggested fix
 	// Collect addresses from this transaction and store in map[string]struct{}
 	addresses := decodedMsg.CollectAllAddresses()
 	mu.Lock()
 	for _, address := range addresses {
 		(*addressesMap)[address] = struct{}{}
 	}
-	decodedMsgs[idx] = decodedMsg
 	mu.Unlock()
+	decodedMsgs[idx] = decodedMsg
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/data_processor/operator.go` around lines 478 - 486, The critical
section around mu.Lock()/mu.Unlock() is larger than necessary: after collecting
addresses via decodedMsg.CollectAllAddresses() you should only protect writes to
the shared addressesMap; move the per-goroutine assignment decodedMsgs[idx] =
decodedMsg to execute outside the locked region so the lock only wraps the loop
that writes (*addressesMap)[address] = struct{}{} (i.e., call mu.Lock(), write
into addressesMap, mu.Unlock(), then set decodedMsgs[idx] = decodedMsg).

704-711: Consider pre-estimating slice capacity.

The slice is created with zero capacity and will grow through multiple append operations. Since you already have the message counts, you could estimate a capacity to reduce allocations.

♻️ Suggested improvement
 func createAddressTx(msgGroups *decoder.DbMessageGroups) []sqlDataTypes.AddressTx {
 	msgSendCount := len(msgGroups.MsgSend)
 	msgCallCount := len(msgGroups.MsgCall)
 	msgAddPkgCount := len(msgGroups.MsgAddPkg)
 	msgRunCount := len(msgGroups.MsgRun)
-	addresses := make([]sqlDataTypes.AddressTx, 0)
+	// Estimate capacity (each message produces at least one AddressTx)
+	estimatedCap := msgSendCount + msgCallCount + msgAddPkgCount + msgRunCount
+	addresses := make([]sqlDataTypes.AddressTx, 0, estimatedCap)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/data_processor/operator.go` around lines 704 - 711, createAddressTx
currently builds addresses with make([]sqlDataTypes.AddressTx, 0) and repeatedly
appends, which causes extra allocations; compute totalCount :=
len(msgGroups.MsgSend)+len(msgGroups.MsgCall)+len(msgGroups.MsgAddPkg)+len(msgGroups.MsgRun)
(or use the existing msgSendCount/msgCallCount/msgAddPkgCount/msgRunCount
variables) and initialize addresses with make([]sqlDataTypes.AddressTx, 0,
totalCount) before appending so the slice has pre-allocated capacity and avoids
growth overhead.

106-129: Consider batching mutex operations to reduce lock contention.

The current implementation acquires and releases the mutex for each precommit individually, which increases contention when processing many blocks concurrently. Collecting all addresses locally first and then performing a single locked write would be more efficient.

♻️ Suggested improvement
 func processPrecommits(
 	mu *sync.Mutex,
 	addressesMap *map[string]struct{},
 	wg *sync.WaitGroup,
 	block *rpcClient.BlockResponse,
 ) {
 	defer wg.Done()
 
+	// Collect addresses locally first
+	localAddrs := make([]string, 0)
 	precommits := block.Result.Block.LastCommit.Precommits
 	for _, precommit := range precommits {
 		if precommit != nil {
-			mu.Lock()
-			(*addressesMap)[precommit.ValidatorAddress] = struct{}{}
-			mu.Unlock()
+			localAddrs = append(localAddrs, precommit.ValidatorAddress)
 		}
 	}
+	localAddrs = append(localAddrs, block.Result.Block.Header.ProposerAddress)
 
-	proposer := block.Result.Block.Header.ProposerAddress
+	// Single locked write
 	mu.Lock()
-	(*addressesMap)[proposer] = struct{}{}
+	for _, addr := range localAddrs {
+		(*addressesMap)[addr] = struct{}{}
+	}
 	mu.Unlock()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/data_processor/operator.go` around lines 106 - 129, In
processPrecommits, avoid locking per precommit by collecting validator addresses
into a local temporary map or slice (e.g., localAddrs) while iterating
precommits and adding the proposer, then acquire mu once and merge localAddrs
into the shared addressesMap before unlocking; use the existing symbols
precommits, proposer, addressesMap, and mu to locate the code and ensure
wg.Done() remains deferred.
pkgs/database/query_view.go (1)

311-323: Extract validator-ID resolution into a shared helper.

The address→ID lookup query is duplicated in two methods. A small helper will keep error handling and query behavior consistent.

♻️ Suggested refactor sketch
+func (t *TimescaleDb) resolveValidatorID(ctx context.Context, validatorAddress, chainName string) (int32, error) {
+	query := `
+	SELECT id
+	FROM gno_validators
+	WHERE address = $1
+	  AND chain_name = $2
+	`
+	var validatorID int32
+	if err := t.pool.QueryRow(ctx, query, validatorAddress, chainName).Scan(&validatorID); err != nil {
+		return 0, fmt.Errorf("validator seems to not exist: %w", err)
+	}
+	return validatorID, nil
+}

Also applies to: 363-376

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/query_view.go` around lines 311 - 323, Extract the duplicated
address→ID lookup into a new helper (e.g., getValidatorIDByAddress or
resolveValidatorID) that accepts context, validatorAddress and chainName and
returns (int32, error); move the SQL, QueryRow, Scan and error-wrapping logic
from the blocks around the id lookup (the code using query1/row1/validatorId)
into this helper and replace both occurrences (the block at the current snippet
and the similar one at lines ~363-376) with calls to the helper, ensuring you
preserve the same error messages and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/key_cmd.go`:
- Around line 257-289: The list command (keyListCmd) opens a DB via connectKeyDb
but never closes it, causing a connection leak; after successfully obtaining db
(the variable returned from connectKeyDb) add a deferred close call (defer
db.Close()) immediately before creating the context so the database is always
closed when RunE returns, ensuring resources used by db.ListApiKeys are
released; place the defer right after the err check that follows connectKeyDb to
cover all return paths in RunE.
- Around line 205-255: The disable and enable CLI handlers (keyDisableCmd and
keyEnableCmd) open a DB via connectKeyDb but never close it; after successfully
obtaining db (the value returned by connectKeyDb in both RunE functions) add a
deferred close (e.g., defer db.Close()) immediately after the error check so the
connection is always released before function exit; apply the same fix in both
keyDisableCmd and keyEnableCmd to mirror keyCreateCmd's behavior.
- Around line 145-155: connectKeyDb returns a *database.TimescaleDb (wrapping a
connection pool) but the four key command handlers (keyCreateCmd, keyDisableCmd,
keyEnableCmd, keyListCmd) never close the pool, leaking connections; after
calling connectKeyDb in each handler, immediately defer closing the pool via
db.GetPool().Close() (or call Close() at the end of the handler if defer isn't
appropriate), ensuring you handle nil/db error cases (only defer when db != nil)
and keep the rest of the handler logic unchanged.

In `@indexer/address_cache/address.go`:
- Around line 130-142: The ctx is cancelled immediately after creation (ctx,
cancel := context.WithTimeout(...); cancel()), so InsertAddresses always
receives a cancelled context; move the cancel call to after the database call
(or use defer cancel() right after creating ctx if you change loop structure) so
that a.db.InsertAddresses(ctx, addresses, chainName, insertValidators) runs with
a valid context, then call cancel() (or let defer run) before the next
iteration; adjust the block around InsertAddresses and insertOneByOne to ensure
cancel() is invoked after the operation completes.
- Around line 108-110: The call to FindExistingAccounts in address.go currently
swallows errors by returning nil which prevents insertWithRetry from attempting
any inserts; update the error path in the block that checks "if err != nil" to
log the error (use the existing logger/context) and return the original
addresses slice (or otherwise propagate the error) so insertWithRetry still
processes addresses; reference the FindExistingAccounts call and the
insertWithRetry flow to ensure you preserve diagnostics and allow insertion
attempts instead of silently skipping them.

In `@indexer/data_processor/operator.go`:
- Around line 388-395: Remove the dead bounds-check inside the loop: the `if idx
>= transactionAmount { wg.Done(); continue }` is always false because
`transactionAmount = len(transactions)` and `idx` comes from `range
transactions`; delete that `if` block and its `wg.Done()` call and simply launch
the goroutine `go d.processMessageGroup(idx, transaction, allDecodedMsgs[idx],
&wg, &msgResults)` for each iteration. Ensure the `wg` counter (Add/Done pairs)
remains correct elsewhere after removing the skipped-branch.
- Around line 176-211: ProcessBlocks is currently vulnerable because
processBlock returns on error leaving zero-value entries in blocksData; modify
processBlock to accept a parallel valid []bool (or *bool pointer per index) and
set valid[idx]=true only on successful parsing (after decoding hash and parsing
height), and ensure the caller ProcessBlocks allocates and passes that valid
slice and filters blocksData to only include entries where valid is true before
DB insert (mirror the valid[] pattern used in ProcessTransactions); update
function signatures: processBlock(..., blocksData []sqlDataTypes.Blocks, valid
[]bool) and set valid[idx]=true at the end on success.

In `@indexer/db_init/hypertable.go`:
- Around line 93-104: The comment for the exported method is stale and has a
typo: replace the incorrect header "AddCompressionPolicy" and fix "TThis" so the
doc matches the actual method name AddColumnstorePolicy on type DBInitializer;
update the comment text to correctly describe that AddColumnstorePolicy adds the
columnstore policy for given tables and remove the duplicate/incorrect
characters so generated docs and linters reflect the correct symbol name and
wording.

In `@pkgs/database/key_mgmt.go`:
- Around line 37-61: GetAllApiKeys is missing a rows.Err() check after iterating
rows; after the for rows.Next() loop (and before returning apiKeys) call if err
:= rows.Err(); err != nil { return nil, err } so iteration errors (e.g.,
network) are surfaced—mirror the pattern used in
GetAllApiKeysWithLimits/ListApiKeys and keep the existing defer rows.Close().

In `@pkgs/database/query_block.go`:
- Around line 152-154: The SQL in GetLastXBlocks uses "block_height >= (SELECT
MAX(height) ...) - $2" which yields x+1 rows; change the lower bound to compute
MAX - $2 + 1 so the range is inclusive and returns exactly x rows (i.e. use
"block_height >= (SELECT MAX(height) FROM blocks WHERE chain_name = $1) - $2 +
1"). Update the query in the GetLastXBlocks code path accordingly so it matches
the intended x-sized window (and remains ordered DESC).
- Around line 83-90: GetLatestBlock (and similarly GetLastXBlocks) currently
runs query1 (ORDER BY height DESC LIMIT 1) and query2 (which uses SELECT
MAX(height) in its WHERE) concurrently which can return mismatched heights if a
new block is inserted mid-run; change the flow to first fetch the block height
from query1 (use the returned blocks[0].Height) and then run the transaction
query (query2) using that concrete height as a parameter instead of the MAX()
subquery so both queries see the same height; apply the same pattern in
GetLastXBlocks by computing the exact height(s) up front and passing them as
parameters to the tx query rather than embedding MAX() twice so you don’t get
off-by-one or extra tx rows.

In `@pkgs/database/query_view.go`:
- Around line 53-60: Queries that SUM() over empty result sets can return NULL
and break scanning into int64s; update the SQL in GetBlockCount24h,
GetBlockCountByDate, GetTotalTxCount, and GetTotalTxCount24h to wrap the
aggregate with COALESCE so it returns 0 instead of NULL (e.g. change
SUM(block_count) to COALESCE(SUM(block_count), 0) and SUM(transaction_count) to
COALESCE(SUM(transaction_count), 0)) so the scanned values into the int64
variables succeed.
- Around line 331-332: The signing_rate_pct expression can be NULL when total
blocks is zero and will break scanning into the non-nullable float64 field
ValidatorSigning.SigningRate; change the SQL to return 0 instead of NULL by
wrapping the rounded expression with COALESCE (e.g. COALESCE(round(...), 0) AS
signing_rate_pct) (or coerce to 0.0) and apply the same change in both
validator-signing queries so the scan into ValidatorSigning.SigningRate never
receives NULL.
- Around line 380-393: The query in GetValidatorSigningByHour uses unqualified
time_bucket references causing ambiguous column errors when joining
validator_signing_counter and block_counter; qualify the time_bucket argument in
time_bucket_gapfill('1 hour', time_bucket) and in the GROUP BY time_bucket('1
hour', time_bucket) to reference the intended table (e.g., vsc.time_bucket) so
the expressions become time_bucket_gapfill('1 hour', vsc.time_bucket) and GROUP
BY time_bucket('1 hour', vsc.time_bucket), ensuring all time_bucket uses
consistently reference validator_signing_counter (vsc).

---

Duplicate comments:
In `@indexer/address_cache/address.go`:
- Around line 148-155: The context is being cancelled immediately after creation
which makes every call to a.db.InsertAddresses fail; move the cancel() call so
it runs after the InsertAddresses call (and its error handling) instead of
directly after context.WithTimeout — create ctx, cancel :=
context.WithTimeout(...), call a.db.InsertAddresses(ctx, ...), handle any error,
then call cancel() (or ensure cancel is invoked in a defer scoped to just that
iteration) to properly release the timeout context.

In `@indexer/db_init/hypertable.go`:
- Around line 73-80: The ALTER TABLE SQL in hypertable.go builds a string via
fmt.Sprintf that injects columnsString into the timescaledb.segmentby clause
without quoting, causing invalid SQL; update the fmt.Sprintf call that
constructs sql (the ALTER TABLE ... SET (...) block) to emit a properly quoted
string literal for timescaledb.segmentby (e.g., wrap the %s in single quotes or
otherwise escape/quote columnsString) so the generated ALTER TABLE uses
timescaledb.segmentby = '<columns>' instead of unquoted raw text.

---

Nitpick comments:
In `@indexer/data_processor/operator.go`:
- Around line 478-486: The critical section around mu.Lock()/mu.Unlock() is
larger than necessary: after collecting addresses via
decodedMsg.CollectAllAddresses() you should only protect writes to the shared
addressesMap; move the per-goroutine assignment decodedMsgs[idx] = decodedMsg to
execute outside the locked region so the lock only wraps the loop that writes
(*addressesMap)[address] = struct{}{} (i.e., call mu.Lock(), write into
addressesMap, mu.Unlock(), then set decodedMsgs[idx] = decodedMsg).
- Around line 704-711: createAddressTx currently builds addresses with
make([]sqlDataTypes.AddressTx, 0) and repeatedly appends, which causes extra
allocations; compute totalCount :=
len(msgGroups.MsgSend)+len(msgGroups.MsgCall)+len(msgGroups.MsgAddPkg)+len(msgGroups.MsgRun)
(or use the existing msgSendCount/msgCallCount/msgAddPkgCount/msgRunCount
variables) and initialize addresses with make([]sqlDataTypes.AddressTx, 0,
totalCount) before appending so the slice has pre-allocated capacity and avoids
growth overhead.
- Around line 106-129: In processPrecommits, avoid locking per precommit by
collecting validator addresses into a local temporary map or slice (e.g.,
localAddrs) while iterating precommits and adding the proposer, then acquire mu
once and merge localAddrs into the shared addressesMap before unlocking; use the
existing symbols precommits, proposer, addressesMap, and mu to locate the code
and ensure wg.Done() remains deferred.

In `@pkgs/database/query_view.go`:
- Around line 311-323: Extract the duplicated address→ID lookup into a new
helper (e.g., getValidatorIDByAddress or resolveValidatorID) that accepts
context, validatorAddress and chainName and returns (int32, error); move the
SQL, QueryRow, Scan and error-wrapping logic from the blocks around the id
lookup (the code using query1/row1/validatorId) into this helper and replace
both occurrences (the block at the current snippet and the similar one at lines
~363-376) with calls to the helper, ensuring you preserve the same error
messages and behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 562c30b0-a38d-4ec1-b70f-d4bec1b10a4e

📥 Commits

Reviewing files that changed from the base of the PR and between e908881 and 230dec4.

📒 Files selected for processing (12)
  • api/config/types.go
  • api/key_cmd.go
  • api/ratelimit/ratelimit.go
  • api/ratelimit/ratelimit_test.go
  • api/serve.go
  • config-api.yml.example
  • indexer/address_cache/address.go
  • indexer/data_processor/operator.go
  • indexer/db_init/hypertable.go
  • pkgs/database/key_mgmt.go
  • pkgs/database/query_block.go
  • pkgs/database/query_view.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • config-api.yml.example
  • api/config/types.go
  • api/ratelimit/ratelimit_test.go
  • api/serve.go

Comment thread api/key_cmd.go
Comment on lines +145 to +155
func connectKeyDb(cmd *cobra.Command) (*database.TimescaleDb, error) {
params, err := parseKeyDbFlags(cmd)
if err != nil {
return nil, err
}
params.password, err = keyPromptPassword()
if err != nil {
return nil, err
}
return database.NewTimescaleDbSetup(params.createDatabaseConfig()), nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Close method on TimescaleDb
ast-grep --pattern $'func ($_ *TimescaleDb) Close() $_ {
  $$$
}'

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 68


🏁 Script executed:

# Find the database.TimescaleDb type definition
find . -name "*.go" -type f | xargs grep -l "type TimescaleDb" | head -5

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 110


🏁 Script executed:

# Also search for the database package structure
find . -path "*/database/*.go" -type f

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 407


🏁 Script executed:

# Search for Close methods in the database package
find . -path "*/database/*.go" -type f -exec grep -l "Close" {} \;

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 327


🏁 Script executed:

# Read TimescaleDb type definition
cat -n ./pkgs/database/types.go | head -100

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 941


🏁 Script executed:

# Search for Close method in database files, showing context
rg "func.*Close" ./pkgs/database/ -A 3

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 68


🏁 Script executed:

# Check the key_mgmt.go file to see how connections are handled
cat -n ./pkgs/database/key_mgmt.go

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 5158


🏁 Script executed:

# Look at pool.go to see connection management
cat -n ./pkgs/database/pool.go

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 7062


🏁 Script executed:

# Search for actual Close calls in the codebase, not just definitions
rg "\.Close\(\)" --type go -B 2

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 4560


🏁 Script executed:

# Read the actual key_cmd.go file to see the command handlers
cat -n ./api/key_cmd.go

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 9580


Add missing database connection cleanup to all four key command handlers.

connectKeyDb returns a *database.TimescaleDb (wrapping a connection pool), but none of the command handlers close the underlying pool after use. This leaks database connection resources. All four subcommands (create, disable, enable, list) are affected.

🔧 Corrected fix for keyCreateCmd (apply similar pattern to other commands)
 		db, err := connectKeyDb(cmd)
 		if err != nil {
 			return err
 		}
+		defer db.GetPool().Close()

 		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
 		defer cancel()

Note: TimescaleDb does not have a Close() method; use db.GetPool().Close() instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/key_cmd.go` around lines 145 - 155, connectKeyDb returns a
*database.TimescaleDb (wrapping a connection pool) but the four key command
handlers (keyCreateCmd, keyDisableCmd, keyEnableCmd, keyListCmd) never close the
pool, leaking connections; after calling connectKeyDb in each handler,
immediately defer closing the pool via db.GetPool().Close() (or call Close() at
the end of the handler if defer isn't appropriate), ensuring you handle nil/db
error cases (only defer when db != nil) and keep the rest of the handler logic
unchanged.

Comment thread api/key_cmd.go
Comment on lines +205 to +255
var keyDisableCmd = &cobra.Command{
Use: "disable",
Short: "Disable an API key by name",
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
if name == "" {
return fmt.Errorf("--name is required")
}

db, err := connectKeyDb(cmd)
if err != nil {
return err
}

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

if err := db.DisableKeyByName(ctx, name); err != nil {
return fmt.Errorf("failed to disable key: %w", err)
}

log.Printf("API key %q disabled", name)
return nil
},
}

var keyEnableCmd = &cobra.Command{
Use: "enable",
Short: "Enable an API key by name",
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
if name == "" {
return fmt.Errorf("--name is required")
}

db, err := connectKeyDb(cmd)
if err != nil {
return err
}

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

if err := db.EnableKeyByName(ctx, name); err != nil {
return fmt.Errorf("failed to enable key: %w", err)
}

log.Printf("API key %q enabled", name)
return nil
},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Database connection leak in disable/enable commands.

Same issue as keyCreateCmd — the database connection should be closed after use.

🔧 Proposed fix for keyDisableCmd and keyEnableCmd
 		db, err := connectKeyDb(cmd)
 		if err != nil {
 			return err
 		}
+		defer db.Close()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var keyDisableCmd = &cobra.Command{
Use: "disable",
Short: "Disable an API key by name",
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
if name == "" {
return fmt.Errorf("--name is required")
}
db, err := connectKeyDb(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := db.DisableKeyByName(ctx, name); err != nil {
return fmt.Errorf("failed to disable key: %w", err)
}
log.Printf("API key %q disabled", name)
return nil
},
}
var keyEnableCmd = &cobra.Command{
Use: "enable",
Short: "Enable an API key by name",
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
if name == "" {
return fmt.Errorf("--name is required")
}
db, err := connectKeyDb(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := db.EnableKeyByName(ctx, name); err != nil {
return fmt.Errorf("failed to enable key: %w", err)
}
log.Printf("API key %q enabled", name)
return nil
},
}
var keyDisableCmd = &cobra.Command{
Use: "disable",
Short: "Disable an API key by name",
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
if name == "" {
return fmt.Errorf("--name is required")
}
db, err := connectKeyDb(cmd)
if err != nil {
return err
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := db.DisableKeyByName(ctx, name); err != nil {
return fmt.Errorf("failed to disable key: %w", err)
}
log.Printf("API key %q disabled", name)
return nil
},
}
var keyEnableCmd = &cobra.Command{
Use: "enable",
Short: "Enable an API key by name",
RunE: func(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
if name == "" {
return fmt.Errorf("--name is required")
}
db, err := connectKeyDb(cmd)
if err != nil {
return err
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := db.EnableKeyByName(ctx, name); err != nil {
return fmt.Errorf("failed to enable key: %w", err)
}
log.Printf("API key %q enabled", name)
return nil
},
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/key_cmd.go` around lines 205 - 255, The disable and enable CLI handlers
(keyDisableCmd and keyEnableCmd) open a DB via connectKeyDb but never close it;
after successfully obtaining db (the value returned by connectKeyDb in both RunE
functions) add a deferred close (e.g., defer db.Close()) immediately after the
error check so the connection is always released before function exit; apply the
same fix in both keyDisableCmd and keyEnableCmd to mirror keyCreateCmd's
behavior.

Comment thread api/key_cmd.go
Comment on lines +257 to +289
var keyListCmd = &cobra.Command{
Use: "list",
Short: "List all API keys",
RunE: func(cmd *cobra.Command, args []string) error {
db, err := connectKeyDb(cmd)
if err != nil {
return err
}

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

keys, err := db.ListApiKeys(ctx)
if err != nil {
return fmt.Errorf("failed to list keys: %w", err)
}

if len(keys) == 0 {
log.Println("No API keys found.")
return nil
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "PREFIX\tNAME\tRPM LIMIT\tACTIVE")
fmt.Fprintln(w, "------\t----\t---------\t------")
for _, k := range keys {
fmt.Fprintf(w, "%s\t%s\t%d\t%v\n", k.Prefix, k.Name, k.RpmLimit, k.IsActive)
}
w.Flush()

return nil
},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Database connection leak in list command.

Same pattern — add defer db.Close() after obtaining the connection.

🔧 Proposed fix
 		db, err := connectKeyDb(cmd)
 		if err != nil {
 			return err
 		}
+		defer db.Close()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var keyListCmd = &cobra.Command{
Use: "list",
Short: "List all API keys",
RunE: func(cmd *cobra.Command, args []string) error {
db, err := connectKeyDb(cmd)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
keys, err := db.ListApiKeys(ctx)
if err != nil {
return fmt.Errorf("failed to list keys: %w", err)
}
if len(keys) == 0 {
log.Println("No API keys found.")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "PREFIX\tNAME\tRPM LIMIT\tACTIVE")
fmt.Fprintln(w, "------\t----\t---------\t------")
for _, k := range keys {
fmt.Fprintf(w, "%s\t%s\t%d\t%v\n", k.Prefix, k.Name, k.RpmLimit, k.IsActive)
}
w.Flush()
return nil
},
}
var keyListCmd = &cobra.Command{
Use: "list",
Short: "List all API keys",
RunE: func(cmd *cobra.Command, args []string) error {
db, err := connectKeyDb(cmd)
if err != nil {
return err
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
keys, err := db.ListApiKeys(ctx)
if err != nil {
return fmt.Errorf("failed to list keys: %w", err)
}
if len(keys) == 0 {
log.Println("No API keys found.")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "PREFIX\tNAME\tRPM LIMIT\tACTIVE")
fmt.Fprintln(w, "------\t----\t---------\t------")
for _, k := range keys {
fmt.Fprintf(w, "%s\t%s\t%d\t%v\n", k.Prefix, k.Name, k.RpmLimit, k.IsActive)
}
w.Flush()
return nil
},
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/key_cmd.go` around lines 257 - 289, The list command (keyListCmd) opens a
DB via connectKeyDb but never closes it, causing a connection leak; after
successfully obtaining db (the variable returned from connectKeyDb) add a
deferred close call (defer db.Close()) immediately before creating the context
so the database is always closed when RunE returns, ensuring resources used by
db.ListApiKeys are released; place the defer right after the err check that
follows connectKeyDb to cover all return paths in RunE.

Comment on lines 108 to +110
if err != nil {
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent error swallowing loses diagnostic information and may skip valid inserts.

When FindExistingAccounts fails, returning nil causes insertWithRetry to skip all addresses. At minimum, log the error for debugging. Consider returning the original addresses slice so insertion is still attempted.

🐛 Proposed fix to log the error and preserve addresses for insertion
 	existing, err := a.db.FindExistingAccounts(ctx, addresses, chainName, insertValidators)
 	if err != nil {
-		return nil
+		l.Error().Caller().Stack().Err(err).Msg("error querying existing accounts")
+		return addresses // still attempt to insert all
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err != nil {
return nil
}
existing, err := a.db.FindExistingAccounts(ctx, addresses, chainName, insertValidators)
if err != nil {
l.Error().Caller().Stack().Err(err).Msg("error querying existing accounts")
return addresses // still attempt to insert all
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/address_cache/address.go` around lines 108 - 110, The call to
FindExistingAccounts in address.go currently swallows errors by returning nil
which prevents insertWithRetry from attempting any inserts; update the error
path in the block that checks "if err != nil" to log the error (use the existing
logger/context) and return the original addresses slice (or otherwise propagate
the error) so insertWithRetry still processes addresses; reference the
FindExistingAccounts call and the insertWithRetry flow to ensure you preserve
diagnostics and allow insertion attempts instead of silently skipping them.

Comment on lines +176 to +211
// processBlock is a helper method to process a block and store it at a pre-allocated slice.
func (d *DataProcessor) processBlock(
idx int,
block *rpcClient.BlockResponse,
wg *sync.WaitGroup,
blocksData []sqlDataTypes.Blocks,
) {
defer wg.Done()
hash, err := base64.StdEncoding.DecodeString(block.Result.BlockMeta.BlockID.Hash)
if err != nil {
l.Error().
Caller().
Stack().
Msgf(
"Failed to decode block hash %s: %v", block.Result.BlockMeta.BlockID.Hash, err,
)
return
}
height, err := strconv.ParseUint(block.Result.Block.Header.Height, 10, 64)
if err != nil {
l.Error().
Caller().
Stack().
Msgf(
"Failed to parse block height %s: %v", block.Result.Block.Header.Height, err,
)
return
}
blocksData[idx] = sqlDataTypes.Blocks{
Hash: hash,
Height: height,
Timestamp: block.Result.Block.Header.Time,
ChainID: block.Result.Block.Header.ChainID,
ChainName: d.chainName,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Failed block processing leaves zero-value entries in the result slice.

When processBlock encounters an error (hash decode or height parse failure), it returns early without marking the slot as invalid. Unlike ProcessTransactions which uses a valid[] slice to filter out failed entries, ProcessBlocks will include zero-initialized sqlDataTypes.Blocks entries in the database insert.

This could insert blocks with zero Height, empty Hash, and zero-value Timestamp into the database.

🛠️ Suggested fix: Add a valid slice pattern consistent with ProcessTransactions
 func (d *DataProcessor) ProcessBlocks(blocks []*rpcClient.BlockResponse, fromHeight uint64, toHeight uint64) {
 	blockAmount := len(blocks)
 	blocksData := make([]sqlDataTypes.Blocks, blockAmount)
+	valid := make([]bool, blockAmount)
 	wg := sync.WaitGroup{}
 	wg.Add(blockAmount)

 	for idx, block := range blocks {
-		go d.processBlock(idx, block, &wg, blocksData)
+		go d.processBlock(idx, block, &wg, &valid[idx], blocksData)
 	}

 	wg.Wait()

+	// Collect only the entries that were successfully processed
+	result := make([]sqlDataTypes.Blocks, 0, blockAmount)
+	for idx, ok := range valid {
+		if ok {
+			result = append(result, blocksData[idx])
+		}
+	}
+
 	timeout := 10*time.Second + (time.Duration(blockAmount) * time.Second / 5)
 	// ... rest of function uses result instead of blocksData

And update processBlock to set *valid = true at the end on success.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/data_processor/operator.go` around lines 176 - 211, ProcessBlocks is
currently vulnerable because processBlock returns on error leaving zero-value
entries in blocksData; modify processBlock to accept a parallel valid []bool (or
*bool pointer per index) and set valid[idx]=true only on successful parsing
(after decoding hash and parsing height), and ensure the caller ProcessBlocks
allocates and passes that valid slice and filters blocksData to only include
entries where valid is true before DB insert (mirror the valid[] pattern used in
ProcessTransactions); update function signatures: processBlock(..., blocksData
[]sqlDataTypes.Blocks, valid []bool) and set valid[idx]=true at the end on
success.

Comment on lines +83 to +90
query2 := `
SELECT
encode(tx_hash, 'base64'),
block_height
FROM transaction_general
WHERE chain_name = $1
AND block_height = (SELECT MAX(height) FROM blocks WHERE chain_name = $1)
`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify concurrent latest-block + independent MAX(height) query pattern
rg -n -C3 "GetLatestBlock|errgroup\\.WithContext|SELECT MAX\\(height\\) FROM blocks WHERE chain_name = \\$1|fetchTransactionData\\(ctx, query2, chainName\\)" pkgs/database/query_block.go

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 924


🏁 Script executed:

cat -n pkgs/database/query_block.go | head -130

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 3819


🏁 Script executed:

sed -n '122,240p' pkgs/database/query_block.go

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 2926


🏁 Script executed:

# Check exact line numbers for query2 in GetLastXBlocks
sed -n '155,175p' pkgs/database/query_block.go | cat -n

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 619


🏁 Script executed:

# Get complete line numbers for GetLastXBlocks query2 and concurrent execution
sed -n '138,190p' pkgs/database/query_block.go | cat -n

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 1550


GetLatestBlock can return mismatched txs due to concurrent MAX(height) evaluation.

query1 (line 73-82: ORDER BY height DESC LIMIT 1) and query2 (line 89: independent SELECT MAX(height)) run concurrently in separate goroutines. If a new block is inserted between their executions, they observe different latest heights. This causes blocks[0].Height to not match the height in the txs map, resulting in missing or misattached transactions.

The same pattern affects GetLastXBlocks (lines 155-156), where two separate MAX() subqueries in the WHERE clause can diverge, and GetLastXBlocks is additionally vulnerable to returning x+1 transaction rows due to MAX() shifting during concurrent execution.

Fix by computing the block height once before fetching transactions:

Suggested fix for GetLatestBlock

Replace the concurrent execution with sequential fetching: retrieve the block height from query1, then use that height as a parameter to query2 instead of a dynamic MAX() subquery.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/query_block.go` around lines 83 - 90, GetLatestBlock (and
similarly GetLastXBlocks) currently runs query1 (ORDER BY height DESC LIMIT 1)
and query2 (which uses SELECT MAX(height) in its WHERE) concurrently which can
return mismatched heights if a new block is inserted mid-run; change the flow to
first fetch the block height from query1 (use the returned blocks[0].Height) and
then run the transaction query (query2) using that concrete height as a
parameter instead of the MAX() subquery so both queries see the same height;
apply the same pattern in GetLastXBlocks by computing the exact height(s) up
front and passing them as parameters to the tx query rather than embedding MAX()
twice so you don’t get off-by-one or extra tx rows.

Comment on lines +152 to +154
AND block_height <= (SELECT MAX(height) FROM blocks WHERE chain_name = $1)
AND block_height >= (SELECT MAX(height) FROM blocks WHERE chain_name = $1) - $2
ORDER BY block_height DESC

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify lower-bound expression in GetLastXBlocks tx range query
rg -n -C2 "GetLastXBlocks|block_height >= \\(SELECT MAX\\(height\\) FROM blocks WHERE chain_name = \\$1\\) - \\$2" pkgs/database/query_block.go

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 316


🏁 Script executed:

cat -n pkgs/database/query_block.go | sed -n '145,160p'

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 596


🏁 Script executed:

rg -n "GetLastXBlocks" pkgs/database/query_block.go -A 10

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 721


🏁 Script executed:

rg -n "block_height >=" pkgs/database/query_block.go -B 2 -A 2

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 464


GetLastXBlocks transaction query pulls one extra height window.

The query at line 153 with block_height >= MAX(height) - $2 and block_height <= MAX(height) creates a range of size x+1, not x. For example, with MAX=100 and x=3, the current condition produces >= 97 AND <= 100, yielding 4 blocks instead of 3. This mismatches query1 (line 136–144), which correctly fetches exactly x blocks using LIMIT $2.

Suggested fix
- AND block_height >= (SELECT MAX(height) FROM blocks WHERE chain_name = $1) - $2
+ AND block_height >= (SELECT MAX(height) FROM blocks WHERE chain_name = $1) - ($2 - 1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AND block_height <= (SELECT MAX(height) FROM blocks WHERE chain_name = $1)
AND block_height >= (SELECT MAX(height) FROM blocks WHERE chain_name = $1) - $2
ORDER BY block_height DESC
AND block_height <= (SELECT MAX(height) FROM blocks WHERE chain_name = $1)
AND block_height >= (SELECT MAX(height) FROM blocks WHERE chain_name = $1) - ($2 - 1)
ORDER BY block_height DESC
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/query_block.go` around lines 152 - 154, The SQL in
GetLastXBlocks uses "block_height >= (SELECT MAX(height) ...) - $2" which yields
x+1 rows; change the lower bound to compute MAX - $2 + 1 so the range is
inclusive and returns exactly x rows (i.e. use "block_height >= (SELECT
MAX(height) FROM blocks WHERE chain_name = $1) - $2 + 1"). Update the query in
the GetLastXBlocks code path accordingly so it matches the intended x-sized
window (and remains ordered DESC).

Comment on lines +53 to +60
query := `
SELECT
SUM(block_count) as block_count
FROM block_counter
WHERE chain_name = $1
AND time_bucket >= NOW() - INTERVAL '24 hours'
AND time_bucket < NOW()
`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify aggregate SUM usage in query_view.go where Scan targets int64
rg -n -C2 "SUM\\((block_count|transaction_count)\\)|Scan\\(&blockCount\\)|Scan\\(&totalTxCount\\)|Scan\\(&txCount24h\\)" pkgs/database/query_view.go

Repository: Cogwheel-Validator/spectra-gnoland-indexer

Length of output: 1353


Wrap aggregate SUM() results with COALESCE() to handle empty result sets.

PostgreSQL returns NULL for SUM() on empty result sets. Scanning NULL into int64 variables causes an error instead of returning 0. This affects four methods:

  • GetBlockCount24h (line 55): SUM(block_count)COALESCE(SUM(block_count), 0)
  • GetBlockCountByDate (line 21): SUM(block_count)COALESCE(SUM(block_count), 0)
  • GetTotalTxCount (line 111): SUM(transaction_count)COALESCE(SUM(transaction_count), 0)
  • GetTotalTxCount24h (line 131): SUM(transaction_count)COALESCE(SUM(transaction_count), 0)

Note: GetTxCountByDay and GetTxCountByHour already correctly use coalesce() (lines 159, 196).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkgs/database/query_view.go` around lines 53 - 60, Queries that SUM() over
empty result sets can return NULL and break scanning into int64s; update the SQL
in GetBlockCount24h, GetBlockCountByDate, GetTotalTxCount, and
GetTotalTxCount24h to wrap the aggregate with COALESCE so it returns 0 instead
of NULL (e.g. change SUM(block_count) to COALESCE(SUM(block_count), 0) and
SUM(transaction_count) to COALESCE(SUM(transaction_count), 0)) so the scanned
values into the int64 variables succeed.

Comment thread pkgs/database/query_view.go Outdated
Comment thread pkgs/database/query_view.go Outdated
@nman98 nman98 merged commit 7fd959a into Cogwheel-Validator:main Mar 15, 2026
7 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 14, 2026
This was referenced Jun 24, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant