Blokli is a Rust workspace project: an on-chain indexer of HOPR smart contracts and on-chain operations provider.
bloklid— Daemon for indexing HOPR on-chain eventsblokli-api— GraphQL API server for querying indexed datadb/— Database modules (SeaORM entities, migrations, abstractions)
Tech stack: Rust (2021 edition), Tokio, Axum + async-graphql, PostgreSQL (SeaORM), Nix Flakes + just, Ethereum/Gnosis Chain (Alloy)
After making code changes, always run just quick (formats, lints, checks compilation).
README.md— High-level overview and quickstartTESTING.md— Test strategy and commandsdesign/architecture.md— System architecture (conceptual, no code snippets)design/target-api-schema.graphql— Generated GraphQL API schemadesign/target-db-schema.mmd— Target database schema
All just commands must be run within nix develop shell. Run just with no arguments to see available commands.
The runtime-tokio feature flag is required and automatically included in just commands.
- snake_case for functions/variables, PascalCase for types/enums
- 4 spaces indentation
//!for module docs,///for item docs- Explicit type annotations on all public function signatures
All imports at module top level — never inside functions or impl blocks.
Group in order: (1) std::, (2) external crates alphabetically, (3) local crates/modules alphabetically.
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::RwLock;
use crate::config::Config;
use crate::indexer::BlockIndexer;- No wildcard imports (
use module::*) — exception: migration files forsea_orm/sea_query - No inline fully-qualified paths — use imports instead
- Use workspace dependencies from root
Cargo.toml
thiserror::Errorfor custom error typesanyhow::Resultfor application-level errors- Always return
Result<T>for fallible operations - No
unwrap()/expect()in production code
All API error handling is centralized in api/src/errors.rs. Key rules:
- Always use error builder functions — never construct errors inline
- Always use
errors::codes::*constants — never hardcode error code strings - Use
crate::errorsfor everything — codes, builders, and message templates
See api/src/errors.rs for available builders and codes. When a pattern repeats, add a new builder there.
- Use
async/awaitwithtokio::spawnfor concurrency Arc<RwLock<T>>for shared mutable state- No blocking operations in async contexts
Always prefer these over creating new types for addresses, balances, channels, accounts, and crypto primitives:
| Crate | Key Types |
|---|---|
hopr-primitive-types |
Address, Balance<C>, HoprBalance, XDaiBalance, U256, SerializableLog, ToHex, IntoEndian |
hopr-crypto-types |
Hash, OffchainPublicKey, ChainKeypair, OffchainKeypair |
hopr-internal-types |
ChannelEntry, ChannelStatus, AccountEntry, AccountType, AcknowledgedTicket |
hopr-bindings |
Smart contract bindings and event encoding/decoding |
Use prelude modules: hopr_types::primitive::prelude, hopr_types::crypto::prelude. Implement From/TryFrom for DB model conversions
(see db/entity/src/conversions/). Run cargo doc --package <crate-name> --open to explore the full API.
- Checked-in generated schema:
just export-target-api-schema→design/target-api-schema.graphql - Generate a local schema copy:
just export-schema-sqlite→schema.graphql - Use DataLoader pattern for N+1 prevention
- Subscriptions via SSE with keep-alive
Context type safety: All schema context data MUST use newtype wrappers — never raw primitives. Existing wrappers in api/src/schema.rs:
ChainId(u64), NetworkName(String), ExpectedBlockTime(u64), Finality(u16).
- Target schema:
design/target-db-schema.mmd - Attribute names must match target schema
- Use SeaORM entities from
db/entity/src/codegen/ - Use
*_currentdatabase views for latest state (not manualORDER BY ... LIMIT 1) - Batch-fetch accounts with HashMap for O(1) lookup (avoid N+1 patterns)
GraphQL subscriptions use an in-memory async_broadcast event bus via IndexerState (chain/indexer/src/state.rs). Subscriptions follow a
2-phase model: (1) atomically capture a watermark and subscribe to the event bus under a coordination lock, (2) stream matching events to
the client. See api/src/subscription.rs for implementation.
To add a new subscription event: publish a new IndexerEvent variant from the indexer handlers, then filter for it in the subscription
resolver.
Config files: bloklid/src/config.rs (structs) and bloklid/example-config.toml (documentation).
These must stay in sync. When modifying config code, update the example config to match — add/remove/rename fields, update defaults,
document new sections. Exclude #[serde(skip)] fields and auto-generated sections like protocols.
Read design/architecture.md before significant changes. Update it when adding components, changing data flows, modifying schema, or
altering deployment models. Keep it conceptual — no code, CLI commands, or config snippets.
chain/api/src/transaction_store.rs — in-memory only, by design. Lost on restart. Only tracks async/sync mode transactions. Use
on-chain confirmation for permanent records.
- Unit tests:
#[cfg(test)]in same file,#[tokio::test]for async - Test error paths and happy paths
- Mock external dependencies (RPC, database)
- Use
instawith YAML snapshots for asserting complex objects (structs, vectors, nested data):let result = build_channel_entry(&log); insta::assert_yaml_snapshot!(result);
- Prefer
instaover multipleassert_eqcalls on different fields of the same object — a single snapshot captures the entire value - Use simple
assert/assert_eqonly for simple values (a single string, integer, boolean, or error variant):assert_eq!(channel.status, ChannelStatus::Open); assert!(result.is_err());
When to use: E2E transaction flows, GraphQL queries against indexed data, Safe/channel operations, bloklid indexing verification.
When NOT to use: Unit logic, anything not needing a running blockchain/bloklid.
Tests use rstest fixtures with #[serial]. Pattern:
#[rstest]
#[test_log::test(tokio::test)]
#[serial]
async fn test_my_feature(#[future(awt)] fixture: IntegrationFixture) -> Result<()> {
let [sender, recipient] = fixture.sample_accounts::<2>();
// ... build and submit transactions via fixture helpers
Ok(())
}For Safe module transactions in tests, use SafePayloadGenerator from hopr-chain-connector.
Debug: container logs saved to /tmp/blokli-integration/<timestamp>/, use RUST_LOG=debug.
- Wildcard imports, imports inside functions/impl blocks, inline fully-qualified paths
- Creating custom types when HOPR foundation types exist
- Creating custom contract encoding/decoding when
hopr-bindingsprovides it - Missing type annotations on public functions
unwrap()/expect()in production codecopy_from_slicefor fixed-size arrays (usetry_into())- Manual
ORDER BY ... LIMIT 1for current state (use*_currentviews) - Per-row account lookups in loops (batch-fetch with HashMap)
- Blocking in async contexts, hardcoded config values
- Direct DB queries without SeaORM entities
- Validate external inputs, use parameterized queries (SeaORM), no sensitive data in logs, TLS in production
- Connection pooling, proper pagination, DataLoader for N+1, Zstandard compression (>1KB)
- Make changes
just quick(fmt, clippy, check)just testor specific tests- Commit
Three variants: docker-bloklid (production), docker-bloklid-dev (development).
nix build -L .#docker-bloklid-x86_64-linux # amd64
nix build -L .#docker-bloklid-aarch64-linux # arm64 (local only, CI disabled)CI builds on every PR commit and merge. Trivy scans for vulnerabilities. Version formats: version-commit.hash, version-pr.number,
version (release).
- SeaORM · async-graphql · Axum · Alloy