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.
Why Declarative?
Section titled “Why Declarative?”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 account | Use #[map] to link IDL fields to your state |
| Manually track and sum event values | Use #[aggregate(strategy = Sum)] |
| Manage WebSocket connections and state diffs | Define entities and let Hyperstack stream updates |
| Build custom backend services for data | Deploy a stack and use generated SDKs |
Anatomy of a Stack Definition
Section titled “Anatomy of a Stack Definition”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>, }}Key Components
Section titled “Key Components”#[hyperstack]Module — The container for your definition. Theidlargument accepts a single path or an array for multi-program stacks.#[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.#[view]— A view is a projection over an entity’s data. It defines what slice of the stream a client subscribes to. Every entity getsstate(one item by key) andlist(all items) by default.#[view]adds custom sorted or filtered projections on top — likeOreRound/latestwhich streams rounds sorted byround_iddescending.#[derive(Stream)]Structs — Nested structs containing the actual field mappings. Must deriveStream,Debug,Clone,Serialize, andDeserialize.- Primary Key — Every entity needs one (annotated
primary_key). This is how Hyperstack tracks individual entity instances. - Field Mappings — Attributes on struct fields defining where data comes from and how it’s processed.
- Resolvers — Fetch off-chain data (e.g. token metadata) and make it available to transforms.
Mapping Types
Section titled “Mapping Types”Hyperstack provides several mapping attributes to populate your entity fields:
| Attribute | Source | Description |
|---|---|---|
#[map] | Account State | Tracks fields within a Solana account. Updates whenever the account changes. Supports lookup_index(register_from = [...]) for cross-account PDA resolution. |
#[from_instruction] | Instructions | Extracts arguments or account keys from a specific instruction. |
#[aggregate] | Events/Instructions | Computes running values (Sum, Count, etc.) from a stream of events. |
#[event] | Events | Captures specific instructions as a log of events within the entity. |
#[snapshot] | Account State | Captures the entire state of an account at a specific point in time. |
#[computed] | Local Fields | Derives a new value by performing calculations on other fields in the same entity. |
#[derive_from] | Instructions | Populates fields by deriving data from instruction context. |
Population Strategies
Section titled “Population Strategies”When data arrives, Strategies determine how the field value is updated. This is particularly powerful for aggregations.
| Strategy | Behavior |
|---|---|
LastWrite | (Default) Overwrites the field with the latest value. |
SetOnce | Sets the value once and ignores subsequent updates (perfect for IDs). |
Sum | Adds the incoming value to the existing total. |
Count | Increments the total by 1 for every matching event. |
Append | Adds the incoming value to a list (creating an event log). |
Max / Min | Keeps only the highest or lowest value seen. |
Multi-Program Stacks
Section titled “Multi-Program Stacks”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.
Example WebSocket Frames
Section titled “Example WebSocket Frames”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.
Next Steps
Section titled “Next Steps”- Mapping Macros — Deep dive into every mapping attribute and its parameters.
- Aggregation Strategies — Learn how to build complex metrics using different strategies.
- CLI Reference — Learn how to build and deploy your stacks.