Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/hypertekorg/hyperstack/llms.txt

Use this file to discover all available pages before exploring further.

The Hyperstack Rust DSL (Domain Specific Language) is a declarative syntax for defining streaming data pipelines. Using procedural macros, you describe what data you want from Solana, not how to fetch it.

Overview

Instead of writing complex ETL pipelines with manual account parsing and event handling, the DSL lets you define:
  • Entities — Structured data objects that project on-chain state
  • Field Mappings — How data flows from Solana accounts into your entities
  • Aggregations — Computed metrics that update automatically
  • Resolvers — External data (like token metadata) enriched automatically
  • Strategies — How incoming data merges with existing state
The macros transform your Rust code into a JSON-based stack spec (.stack.json), which Hyperstack compiles into optimized bytecode for real-time execution.

Module-Level Macros

#[hyperstack]

The entry point for any stack definition. Must be applied to a pub mod.
#[hyperstack(idl = "idl/ore.json")]
pub mod ore_stream {
    // Entity definitions...
}
Arguments:
ArgumentTypeRequiredDescription
idlstring | arrayNo*Path(s) to Anchor IDL JSON file(s) relative to Cargo.toml. Use an array for multi-program stacks: idl = ["ore.json", "entropy.json"].
protostring | arrayNo*Path(s) to .proto files for Protobuf-based streams.
skip_decodersboolNoIf true, skips generating instruction decoders.
* Either idl or proto must be provided. Multi-program example:
#[hyperstack(idl = ["idl/ore.json", "idl/entropy.json"])]
pub mod ore_stream {
    // Both ore_sdk::* and entropy_sdk::* are available
}

Entity-Level Macros

#[entity]

Defines a struct as a Hyperstack entity (state projection).
#[entity(name = "OreRound")]
pub struct OreRound {
    pub id: RoundId,
    pub state: RoundState,
}
Arguments:
ArgumentTypeRequiredDescription
namestringNoCustom name for the entity. Defaults to the struct name.

#[view]

Defines queryable views for the entity. Every entity gets state and list by default.
#[view(name = "latest", sort_by = "id.round_id", order = "desc")]
Arguments:
ArgumentTypeRequiredDescription
namestringYesView name (used in SDK: views.Entity.name)
sort_bystringNoField path to sort by (e.g., "id.round_id")
orderstringNoSort order: "asc" or "desc" (default: asc)

Field-Level Macros

#[map]

Maps a field from a Solana account to an entity field.
#[map(ore_sdk::accounts::Round::total_deployed, strategy = LastWrite)]
pub total_deployed: Option<u64>,
Arguments:
ArgumentTypeRequiredDescription
frompathYes*Source account field (e.g., AccountType::field_name)
primary_keyboolNoMarks this field as the primary key
lookup_indexbool | fnNoCreates a lookup index (see cross-account resolution below)
strategyStrategyNoUpdate strategy (default: SetOnce)
transformTransformNoTransformation to apply (e.g., Base58Encode, ui_amount(...))
wheninstructionNoOnly update when this instruction occurs
conditionstringNoBoolean expression filter (e.g., "value != ZERO_32")
emitboolNoIf false, field is computed but not sent to clients
stopinstructionNoStop updating when this instruction occurs
stop_lookup_byfieldNoAccount reference for stop instruction
* First positional argument is the source path Primary key example:
#[map(ore_sdk::accounts::Round::id, primary_key, strategy = SetOnce)]
pub round_id: u64,
Transform example:
#[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite,
      transform = ui_amount(ore_metadata.decimals))]
pub motherlode: Option<f64>,
Conditional update example:
#[map(entropy_sdk::accounts::Var::value,
      when = entropy_sdk::instructions::Reveal,
      condition = "value != ZERO_32",
      strategy = LastWrite)]
pub entropy_value: Option<String>,

#[aggregate]

Computes running values from instructions.
#[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
pub deploy_count: Option<u64>,
Arguments:
ArgumentTypeRequiredDescription
frompath | arrayYesInstruction(s) to aggregate from
fieldfieldNoField to aggregate (accounts::name or args::name)
strategyStrategyYesSum, Count, Min, Max, UniqueCount
lookup_byfieldNoAccount reference to resolve entity (e.g., accounts::round)
conditionstringNoBoolean expression (e.g., "amount > 1_000_000")
transformTransformNoTransform before aggregating
Count example:
#[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
pub deploy_count: Option<u64>,
Sum example:
#[aggregate(from = ore_sdk::instructions::Deploy, field = args::amount, strategy = Sum, lookup_by = accounts::round)]
pub total_deployed_amount: Option<u64>,

#[computed]

Derives a field from other fields in the same entity.
#[computed(state.total_deployed.map(|d| d / 1_000_000_000))]
pub total_deployed_sol: Option<u64>,
Available in expressions:
  • Other fields in the entity (by group name: state.field_name, id.field_name)
  • Resolver fields (e.g., ore_metadata.decimals)
  • Special variables: __timestamp, __slot, __signature
  • Resolver methods: TokenMetadata::ui_amount(raw, decimals)
Complex example from ORE stack:
#[computed({
    let expires_at_slot = state.expires_at.unwrap_or(0) as u64;
    let current_slot = __slot;
    if current_slot > 0 && expires_at_slot > current_slot {
        Some(__timestamp + (((expires_at_slot - current_slot) * 400 / 1000) as i64))
    } else {
        None
    }
})]
pub estimated_expires_at_unix: Option<i64>,

#[snapshot]

Captures the entire state of an account.
#[snapshot(strategy = LastWrite, transforms = [(authority, Base58Encode)])]
pub miner_snapshot: Option<ore_sdk::accounts::Miner>,
Arguments:
ArgumentTypeRequiredDescription
frompathNoSource account type (inferred from field type if omitted)
strategyStrategyNoOnly SetOnce or LastWrite allowed
transformsarrayNoList of (field, Transform) tuples for specific sub-fields
Example with transforms:
#[snapshot(strategy = LastWrite, transforms = [(authority, Base58Encode), (executor, Base58Encode)])]
pub automation_snapshot: Option<ore_sdk::accounts::Automation>,

#[event]

Captures multiple fields from an instruction as a structured event.
#[event(
    from = PlaceTrade,
    fields = [amount, accounts::user],
    strategy = Append
)]
pub trades: Vec<TradeEvent>,
Arguments:
ArgumentTypeRequiredDescription
frompathYesThe source instruction type
fieldsarrayYesList of fields to capture (accounts::name, args::name)
strategyStrategyNoUpdate strategy (typically Append)
transformsarrayNoList of (field, Transform) tuples
lookup_byfieldNoAccount reference to resolve entity

#[resolve]

Attaches a resolver to fetch off-chain data.
#[resolve(address = "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp")]
pub ore_metadata: Option<TokenMetadata>,
Arguments:
ArgumentTypeRequiredDescription
addressstringYesThe address to resolve (e.g., mint address)
Available resolvers:
  • TokenMetadata — Fetches SPL token metadata (name, symbol, decimals, logo)
See Resolvers for complete reference.

#[derive_from]

Derives values from instruction metadata.
#[derive_from(from = [Buy, Sell], field = __timestamp)]
pub last_updated: i64,
Special fields:
FieldDescription
__timestampUnix timestamp of the block
__slotSlot number
__signatureTransaction signature (Base58)

Cross-Account Resolution

lookup_index(register_from = [...])

When an entity includes fields from a secondary account (one that doesn’t store the primary key), use register_from to tell Hyperstack how to map addresses.
#[map(entropy_sdk::accounts::Var::end_at,
      lookup_index(register_from = [
          (ore_sdk::instructions::Deploy, accounts::entropyVar, accounts::round),
          (ore_sdk::instructions::Reset, accounts::entropyVar, accounts::round)
      ]),
      strategy = LastWrite)]
pub expires_at: Option<u64>,
Each tuple specifies:
  1. Instruction — When to register the mapping
  2. PDA field — The secondary account address
  3. Primary key field — The entity’s primary key
When Deploy or Reset instructions run, Hyperstack registers entropyVar_address → round_address. Subsequent Var account updates are routed to the correct OreRound entity. Real example from ORE stack:
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundTreasury {
    #[map(ore_sdk::accounts::Treasury::motherlode,
          lookup_index(register_from = [
              (ore_sdk::instructions::Reset, accounts::treasury, accounts::roundNext)
          ]),
          stop = ore_sdk::instructions::Reset,
          stop_lookup_by = accounts::round,
          strategy = SetOnce,
          transform = ui_amount(ore_metadata.decimals))]
    pub motherlode: Option<f64>,
}
This:
  1. Registers treasury → roundNext mapping during Reset instruction
  2. Updates the field with the treasury’s motherlode value (as UI amount)
  3. Stops updating when Reset is called for this specific round

Population Strategies

StrategyBehaviorExample Use
LastWriteOverwrite with latestCurrent balances
SetOnceWrite only if emptyIDs, creation timestamps
SumAdd to existing totalVolume, TVL
CountIncrement by 1Trade count
AppendAdd to listEvent history
Max / MinKeep extreme valuePrice highs/lows
UniqueCountCount distinct valuesActive users
MergeMerge object keysConfiguration maps
See Population Strategies for detailed usage.

Transformations

TransformDescriptionExample
Base58EncodeEncode bytes to Base58 stringtransform = Base58Encode
Base58DecodeDecode Base58 string to bytestransform = Base58Decode
HexEncodeEncode bytes to Hex stringtransform = HexEncode
HexDecodeDecode Hex string to bytestransform = HexDecode
ToStringConvert value to stringtransform = ToString
ToNumberConvert value to numbertransform = ToNumber
ui_amount(...)Convert raw token amount to UI amount using decimalstransform = ui_amount(9)
transform = ui_amount(meta.decimals)
Transform with resolver:
#[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite,
      transform = ui_amount(ore_metadata.decimals))]
pub motherlode: Option<f64>,

Complete Example: OreMiner Entity

From the real ORE stack:
#[entity(name = "OreMiner")]
pub struct OreMiner {
    pub id: MinerId,
    pub rewards: MinerRewards,
    pub state: MinerState,

    #[snapshot(strategy = LastWrite, transforms = [(authority, Base58Encode)])]
    pub miner_snapshot: Option<ore_sdk::accounts::Miner>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct MinerId {
    // Both Miner and Automation accounts share authority as identity
    #[map([ore_sdk::accounts::Miner::authority, ore_sdk::accounts::Automation::authority], 
          primary_key, strategy = SetOnce, transform = Base58Encode)]
    pub authority: String,

    #[map(ore_sdk::accounts::Miner::__account_address, lookup_index, strategy = SetOnce)]
    pub miner_address: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct MinerRewards {
    #[map(ore_sdk::accounts::Miner::rewards_sol, strategy = LastWrite)]
    pub rewards_sol: Option<u64>,

    #[map(ore_sdk::accounts::Miner::rewards_ore, strategy = LastWrite)]
    pub rewards_ore: Option<u64>,

    #[map(ore_sdk::accounts::Miner::lifetime_deployed, strategy = LastWrite)]
    pub lifetime_deployed: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct MinerState {
    #[map(ore_sdk::accounts::Miner::round_id, strategy = LastWrite)]
    pub round_id: Option<u64>,

    #[map(ore_sdk::accounts::Miner::checkpoint_id, strategy = LastWrite)]
    pub checkpoint_id: Option<u64>,
}

Next Steps

Build docs developers (and LLMs) love