Quantum-safe, multi-chain indexer + GraphQL backend for Substrate-based chains using NIST-approved signature schemes (e.g., Dilithium). Chronicle walks each chain from genesis to head, records per-account balance deltas (including miner rewards for PoW), and serves flexible queries via Postgres/TimescaleDB and Hasura.
Chronicle is built for post-quantum Substrate chains. It focuses on:
deadpool-postgreschronicled: Rust daemon that ingests blocks and writes balance factscrates/chron-db: shared DB helpers (schema DDL, repository, models, connection)orchestration/: Podman Quadlet units to run TimescaleDB, Hasura, and chronicledscript/: helper scripts (e.g., base58/hex conversion for chain IDs)Typical layout:
src/main.rs: main indexer loop + runtime discoverysrc/balance_decoder.rs: event decoding and balance change extractionsrc/config.rs, connection.rs, models.rs, repository.rs, schema.rs, error.rsquadlet/: .container and .volume unitsconfig/: env templates per chainRequirements:
mkdir -p ~/.config/containers/systemd
cp -v orchestration/quadlet/*.container orchestration/quadlet/*.volume ~/.config/containers/systemd/
systemctl --user daemon-reload
cp orchestration/config/chronicled-my-chain-id.env.example orchestration/config/chronicled-<base58-genesis-id>.env
# Edit the new env file to point to your node WS URL, DB DSN, etc.
systemctl --user enable --now chronicle-timescaledb.service chronicle-hasura.service
systemctl --user enable --now chronicle-chronicled@<base58-genesis-id>.service
GraphQL will be available at:
chronicled is configured via environment variables (Quadlet env files or process env):
WS_URL: WebSocket endpoint of your quantum-safe Substrate node (e.g., wss://a.t.res.fm)PG_DSN: PostgreSQL DSN (e.g., postgresql:///chronicle or a full URL with auth/host)ENABLE_TIMESCALE: true to enable hypertable creationDB_MAX_CONNECTIONS: maximum DB connections (default 10)DB_MIN_CONNECTIONS: minimum DB connections (default 1)RUST_LOG: log level (error, warn, info, debug, trace; default info)Example local run:
export WS_URL=wss://a.t.res.fm
export PG_DSN=postgresql:///chronicle
export RUST_LOG=info
target/release/chronicled
chronicled unit per chain ID.# Dockerfile
FROM rust:1.70 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/chronicled /usr/local/bin/
CMD ["chronicled"]
Build and run:
docker build -t chronicled .
docker run --rm \
-e WS_URL=wss://a.t.res.fm \
-e PG_DSN=postgres://chronicle:${PG_PASSWORD}@db:5432/chronicle \
-e RUST_LOG=info \
chronicled
cargo build --release
target/release/chronicled.Per-chain isolation:
DROP SCHEMA "<base58-genesis-id>" CASCADE;
Canonical tables (per chain schema):
blocks
number (bigint, PK)hash (bytea)parent_hash (bytea)timestamp (timestamptz)is_canonical (boolean)runtime_spec (bigint)metadata
spec_version (int, PK), impl_version (int), transaction_version (int), state_version (int)first_seen_block (bigint), last_seen_block (bigint null)metadata_bytes (bytea), metadata_hash (bytea)created_at (timestamptz), updated_at (timestamptz)balance_changes
id (bigserial, PK)account (bytea)block_number (bigint)event_index (int)delta (numeric(78,0))reason (text)extrinsic_hash (bytea)event_pallet (text)event_variant (text)block_ts (timestamptz)index_progress
chain_id (text, PK)latest_block (bigint)latest_block_hash (bytea)latest_block_ts (timestamptz)blocks_indexed (bigint)balance_changes_recorded (bigint)started_at (timestamptz), updated_at (timestamptz)account_stats
account (bytea, PK)balance (numeric(78,0))first_seen_block (bigint)last_activity_block (bigint)total_changes (bigint)Helpers:
script/.Indexing progress:
SELECT * FROM "CHAIN_BASE58".index_progress;
Account balance at a specific block:
SELECT SUM(delta::NUMERIC) AS balance
FROM "CHAIN_BASE58".balance_changes
WHERE account = '\xDEADBEEF...'::bytea
AND block_number <= 123456;
Largest balance changes:
SELECT account, block_number, delta, reason, event_variant
FROM "CHAIN_BASE58".balance_changes
ORDER BY ABS(delta::NUMERIC) DESC
LIMIT 10;
Hasura:
You’ll adapt decoding to your runtime’s event structure (recommended: generate static types from metadata).
balance_decoder.rs)fn decode_transfer_event(
&self,
event: &EventDetails<PolkadotConfig>,
block_number: i64,
event_index: i32,
block_timestamp: DateTime<Utc>,
extrinsic_hash: Option<Vec<u8>>,
) -> Result<Vec<BalanceChange>> {
// Decode your chain's balances::Transfer (from, to, amount)
// Return one negative delta for 'from' and one positive for 'to'
}
pub async fn query_genesis_endowments(&self) -> Result<Vec<BalanceChange>> {
// Read System.Account at genesis and create Endowment deltas
}
pub async fn decode_miner_rewards(
&self,
block_hash: [u8; 32],
block_number: i64,
block_timestamp: DateTime<Utc>,
) -> Result<Vec<BalanceChange>> {
// Extract author from digest, determine reward, create BalanceChange
}
subxt-cli:cargo install subxt-cli
subxt metadata -f bytes --url wss://a.t.res.fm > metadata.scale
#[subxt::subxt(runtime_metadata_path = "metadata.scale")]
pub mod runtime {}
// Example:
let transfer = event.as_event::<runtime::balances::events::Transfer>()?;
Prerequisites:
Build:
cargo build --release
Run:
cargo run -p chronicled
Test:
cargo test
Logging:
RUST_LOG=debug,chronicled=trace cargo run -p chronicled
Binary:
target/release/chronicledContributions are welcome. Please:
For questions or issues, open an issue in this repository.
Apache-2.0
8 activities