Skip to content

Stack Definitions

Hyperstack uses a declarative Rust DSL (Domain Specific Language) to define how on-chain Solana data should be transformed, aggregated, and streamed to your application. Instead of writing complex ETL pipelines, you simply declare the final state you want, and Hyperstack handles the rest.


Building data pipelines for Solana typically involves manual account parsing, complex event handling, and managing state synchronization. Hyperstack replaces this imperative approach with a declarative model:

Imperative Approach (Traditional)Declarative Approach (Hyperstack)
Write custom decoding logic for every accountUse #[map] to link IDL fields to your state
Manually track and sum event valuesUse #[aggregate(strategy = Sum)]
Manage WebSocket connections and state diffsDefine entities and let Hyperstack stream updates
Build custom backend services for dataDeploy a stack and use generated SDKs

A Hyperstack definition is a Rust module annotated with #[hyperstack]. Inside this module, you define Entities — the structured data objects your application will consume. A single module can contain multiple #[entity] structs, all packaged into one stack.

The ORE stack is a real example. Here’s a simplified version showing the core concepts with a single IDL:

use hyperstack::prelude::*;
#[hyperstack(idl = "idl/ore.json")]
pub mod ore_stream {
use hyperstack::macros::Stream;
use serde::{Deserialize, Serialize};
// OreRound is the main entity -- one instance per mining round.
// The `latest` view sorts rounds descending by round_id.
#[entity(name = "OreRound")]
#[view(name = "latest", sort_by = "id.round_id", order = "desc")]
pub struct OreRound {
pub id: RoundId,
pub state: RoundState,
pub metrics: RoundMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundId {
// Primary key -- set once, never overwritten
#[map(ore_sdk::accounts::Round::id, primary_key, strategy = SetOnce)]
pub round_id: u64,
#[map(ore_sdk::accounts::Round::__account_address, lookup_index, strategy = SetOnce)]
pub round_address: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundState {
// LastWrite: overwritten each time the account updates
#[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite)]
pub motherlode: Option<u64>,
#[map(ore_sdk::accounts::Round::total_deployed, strategy = LastWrite)]
pub total_deployed: Option<u64>,
// Computed field: derived from other fields in this entity
#[computed(state.total_deployed.map(|d| d / 1_000_000_000))]
pub total_deployed_sol: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundMetrics {
// Aggregate: counts Deploy instructions referencing this round
#[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
pub deploy_count: Option<u64>,
}
}
  1. #[hyperstack] Module — The container for your definition. The idl argument accepts a single path or an array for multi-program stacks.
  2. #[entity] Struct — An entity is an individual definition of some part of your app’s data. Each entity represents a distinct concept — a round, a miner, a treasury — and declares exactly which on-chain fields belong to it, across as many accounts or programs as needed. A stack can have many entities.
  3. #[view] — A view is a projection over an entity’s data. It defines what slice of the stream a client subscribes to. Every entity gets state (one item by key) and list (all items) by default. #[view] adds custom sorted or filtered projections on top — like OreRound/latest which streams rounds sorted by round_id descending.
  4. #[derive(Stream)] Structs — Nested structs containing the actual field mappings. Must derive Stream, Debug, Clone, Serialize, and Deserialize.
  5. Primary Key — Every entity needs one (annotated primary_key). This is how Hyperstack tracks individual entity instances.
  6. Field Mappings — Attributes on struct fields defining where data comes from and how it’s processed.
  7. Resolvers — Fetch off-chain data (e.g. token metadata) and make it available to transforms.

Hyperstack provides several mapping attributes to populate your entity fields:

AttributeSourceDescription
#[map]Account StateTracks fields within a Solana account. Updates whenever the account changes. Supports lookup_index(register_from = [...]) for cross-account PDA resolution.
#[from_instruction]InstructionsExtracts arguments or account keys from a specific instruction.
#[aggregate]Events/InstructionsComputes running values (Sum, Count, etc.) from a stream of events.
#[event]EventsCaptures specific instructions as a log of events within the entity.
#[snapshot]Account StateCaptures the entire state of an account at a specific point in time.
#[computed]Local FieldsDerives a new value by performing calculations on other fields in the same entity.
#[derive_from]InstructionsPopulates fields by deriving data from instruction context.

When data arrives, Strategies determine how the field value is updated. This is particularly powerful for aggregations.

StrategyBehavior
LastWrite(Default) Overwrites the field with the latest value.
SetOnceSets the value once and ignores subsequent updates (perfect for IDs).
SumAdds the incoming value to the existing total.
CountIncrements the total by 1 for every matching event.
AppendAdds the incoming value to a list (creating an event log).
Max / MinKeeps only the highest or lowest value seen.

A single stack can consume data from multiple Solana programs by passing an array of IDL files:

#[hyperstack(idl = ["idl/ore.json", "idl/entropy.json"])]
pub mod ore_stream {
// ore_sdk::accounts::* and ore_sdk::instructions::* are available
// entropy_sdk::accounts::* and entropy_sdk::instructions::* are available
}

Each IDL generates its own namespaced SDK module (e.g., ore_sdk, entropy_sdk). You can then map fields from any program’s accounts and reference instructions from any program in register_from, #[aggregate], #[event], etc.

To link accounts across programs, use lookup_index(register_from = [...]) — see the Macro Reference for the full syntax and examples.


The stack definition above produces WebSocket frames with the following structure. An upsert frame contains the full entity state:

{
"op": "upsert",
"mode": "state",
"entity": "Token",
"key": "So11111111111111111111111111111111111111112",
"data": {
"id": {
"mint": "So11111111111111111111111111111111111111112"
},
"state": {
"reserves": 1500000000,
"tvl": 150000000000
},
"metrics": {
"total_volume": 42000000000
}
}
}

When only specific fields change, a patch frame contains just the updated values:

{
"op": "patch",
"mode": "state",
"entity": "Token",
"key": "So11111111111111111111111111111111111111112",
"data": {
"state": {
"reserves": 1520000000,
"tvl": 152000000000
}
}
}

The SDK merges patches into local state automatically, so your application always sees the complete entity.