Skip to content

Macro Reference

Hyperstack uses Rust procedural macros to define data pipelines declaratively. These macros transform your Rust structs into a unified stack spec (.stack.json), which is then used for both local execution and cloud deployment.


The entry point for any Hyperstack stream definition. It must be applied to a pub mod that contains your entity definitions.

#[hyperstack(idl = "idl.json")]
pub mod my_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 (useful for manual decoding).

* Either idl or proto must be provided.


Defines a struct as a Hyperstack entity (state projection). Each entity results in a separate typed stream.

#[entity(name = "TradeTracker")]
struct Tracker {
// Field mappings...
}

Arguments:

ArgumentTypeRequiredDescription
namestringNoCustom name for the entity. Defaults to the struct name.

These macros are applied to fields within an #[entity] struct to define how data is captured and updated.

Maps a field from a Solana account directly to an entity field.

#[map(pump_sdk::accounts::BondingCurve::virtual_sol_reserves, strategy = LastWrite)]
pub reserves: u64,

The path prefix (pump_sdk::accounts::) is derived from the IDL’s program name. See Stack Definitions for the naming convention.

Arguments:

ArgumentTypeRequiredDescription
frompathYesSource account field (e.g., AccountType::field_name).
primary_keyboolNoMarks this field as the primary key for the entity.
lookup_indexbool | fnNoCreates a lookup index for this field. Accepts an optional register_from parameter for cross-account PDA resolution (see Cross-Account Resolution below).
strategyStrategyNoUpdate strategy (default: SetOnce).
transformTransformNoTransformation to apply before storing.
renamestringNoCustom target field name in the projection.
temporal_fieldstringNoSecondary field for temporal indexing.
join_onstringNoField to join on for multi-entity lookups.

Maps a field from an instruction’s arguments or accounts.

#[from_instruction(PlaceTrade::amount, strategy = Append)]
pub trade_amounts: Vec<u64>,

Arguments: Accepts the same arguments as #[map].

Captures multiple fields from an instruction as a single 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. Use accounts::name for instruction accounts and args::name (or data::name) for arguments.
strategyStrategyNoUpdate strategy (default: SetOnce).
transformsarrayNoList of (field, Transform) tuples for processing captured fields.
lookup_byfieldNoField used to resolve the entity key.
renamestringNoCustom target field name.
join_onfieldNoJoin field for multi-entity lookups.

Captures the entire state of a source account as a snapshot.

#[snapshot(from = BondingCurve, strategy = LastWrite)]
pub latest_state: BondingCurve,

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.
lookup_byfieldNoField used to resolve the entity key.
renamestringNoCustom target field name.
join_onfieldNoJoin field for multi-entity lookups.

Defines a declarative aggregation from instructions.

#[aggregate(from = [Buy, Sell], field = amount, strategy = Sum)]
pub total_volume: u64,

Arguments:

ArgumentTypeRequiredDescription
frompath | arrayYesInstruction(s) to aggregate from.
fieldfieldNoField to aggregate. Use accounts::name or args::name. If omitted, performs Count.
strategyStrategyNoSum, Count, Min, Max, UniqueCount.
conditionstringNoBoolean expression (e.g., "amount > 1_000_000").
transformTransformNoTransform to apply before aggregating.
lookup_byfieldNoField used to resolve the entity key.
renamestringNoCustom target field name.
join_onfieldNoJoin field for multi-entity lookups.

Defines a field derived from other fields in the same entity using a Rust-like expression.

#[computed(total_buy_volume + total_sell_volume)]
pub total_volume: u64,

Arguments: Takes a single Rust expression. Can reference other fields in the entity.

Attaches a resolver to a field. Hyperstack fetches the external data server-side and delivers it as part of the entity — no extra API calls needed from the client.

#[resolve(address = "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp")]
pub ore_metadata: Option<TokenMetadata>,

Arguments:

ArgumentTypeRequiredDescription
addressstringYesThe fixed address to resolve against. For TokenMetadata this is the mint address.

Available resolvers:

ResolverType fieldWhat it fetches
TokenMetadataOption<TokenMetadata>SPL token metadata (name, symbol, decimals, logo) via the DAS API

Once resolved, the data is available to other fields in the same entity. Use #[computed] to derive values from it, or reference the resolver fields directly in #[map] transforms:

// Option A: use resolver decimals in a transform on #[map]
#[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite,
transform = ui_amount(ore_metadata.decimals))]
pub motherlode: Option<f64>,
// Option B: use resolver computed methods in #[computed]
#[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite)]
pub motherlode_raw: Option<u64>,
#[computed(state.motherlode_raw.and_then(|v| ore_metadata.ui_amount(v)))]
pub motherlode_ui: Option<f64>,

Resolver data is cached server-side — metadata is fetched once per address and reused across all entities that reference it.

See Resolvers for the full reference on TokenMetadata fields and computed methods.

Derives values from instruction metadata or arguments.

#[derive_from(from = [Buy, Sell], field = __timestamp)]
pub last_updated: i64,

Arguments:

ArgumentTypeRequiredDescription
frompath | arrayYesInstruction(s) to derive from.
fieldfieldYesTarget field. Can be a special field or a regular instruction arg.
strategyStrategyNoLastWrite or SetOnce.
conditionstringNoBoolean expression for conditional derivation.
transformTransformNoTransform to apply.
lookup_byfieldNoField used to resolve the entity key.

Special Fields:

FieldDescription
__timestampThe Unix timestamp of the block containing the instruction.
__slotThe slot number of the block.
__signatureThe transaction signature (Base58 encoded).

Cross-Account Resolution with register_from

Section titled “Cross-Account Resolution with register_from”

When an entity’s state includes data from a secondary account (one that doesn’t store the entity’s primary key directly), you need a way to tell Hyperstack how to map that account’s address back to an entity instance. The lookup_index(register_from = [...]) syntax on #[map] handles this declaratively.

Consider a Solana program where a BondingCurve PDA is derived from a token mint. When a BondingCurve account update arrives, Hyperstack needs to know which Token entity (keyed by mint) it belongs to. The BondingCurve account itself doesn’t store the mint — the relationship only exists in instructions that reference both accounts together.

Add register_from to a lookup_index field that maps the secondary account’s address:

#[map(pump_sdk::accounts::BondingCurve::__account_address, lookup_index(
register_from = [
(pump_sdk::instructions::Create, accounts::bonding_curve, accounts::mint),
(pump_sdk::instructions::Buy, accounts::bonding_curve, accounts::mint),
(pump_sdk::instructions::Sell, accounts::bonding_curve, accounts::mint)
]
), strategy = SetOnce)]
pub bonding_curve: String,

Each tuple in register_from specifies:

PositionMeaningExample
1stThe instruction type to watchpump_sdk::instructions::Create
2ndThe instruction account containing the PDA addressaccounts::bonding_curve
3rdThe instruction account containing the entity’s primary keyaccounts::mint

When any of these instructions are processed, Hyperstack registers the mapping bonding_curve_address → mint. Subsequent BondingCurve account updates are then routed to the correct Token entity.

register_from also works across programs in multi-IDL stacks. For example, linking an entropy program’s Var account to an ore program’s Round entity:

#[hyperstack(idl = ["idl/ore.json", "idl/entropy.json"])]
pub mod ore_stream {
// ... entity definition ...
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct EntropyState {
#[map(entropy_sdk::accounts::Var::value, strategy = LastWrite, transform = Base58Encode)]
pub entropy_value: Option<String>,
// The lookup_index with register_from links Var accounts to Round entities
#[map(entropy_sdk::accounts::Var::__account_address, lookup_index(
register_from = [
(ore_sdk::instructions::Deploy, accounts::entropyVar, accounts::round),
(ore_sdk::instructions::Reset, accounts::entropyVar, accounts::round)
]
), strategy = SetOnce)]
pub entropy_var_address: Option<String>,
}
}

Here, the Deploy and Reset instructions (from the ore program) reference both the entropyVar account (from the entropy program) and the round account. This is enough for Hyperstack to establish the mapping.


Declarative hooks are struct-level annotations for custom key resolution logic and PDA mappings. For most use cases, prefer lookup_index(register_from = [...]) on field mappings (see above). These hooks are available as escape hatches for advanced scenarios.

Defines how an account’s primary key is resolved. This is essential when an account doesn’t store its “owner” ID directly, but its address can be derived via PDA or looked up in a registry.

#[resolve_key(
account = UserProfile,
strategy = "pda_reverse_lookup",
lookup_name = "user_pda"
)]
struct UserResolver;

Arguments:

ArgumentTypeRequiredDescription
accountpathYesThe account type this resolver applies to.
strategystringNo"pda_reverse_lookup" (default) or "direct_field".
lookup_namestringNoThe name of the registry to use for reverse lookups.
queue_untilarrayNoList of instructions to wait for before resolving (ensures PDA is registered).

Registers a mapping between a PDA address and a primary key during an instruction. This mapping is stored in a temporary registry to enable #[resolve_key] to work for accounts that are created or updated in the same transaction.

#[register_pda(
instruction = CreateUser,
pda_field = accounts::user_pda,
primary_key = args::user_id,
lookup_name = "user_pda"
)]
struct PdaMapper;

Arguments:

ArgumentTypeRequiredDescription
instructionpathYesThe instruction where the PDA is created/referenced.
pda_fieldfieldYesThe field containing the PDA address (e.g., accounts::user_pda).
primary_keyfieldYesThe primary key to associate with this PDA (e.g., args::user_id).
lookup_namestringNoThe name of the registry to store this mapping in.

StrategyDescription
SetOnceOnly write if the field is currently empty.
LastWriteAlways overwrite with the latest value.
AppendAppend to a Vec.
MergeDeep-merge objects (for nested structs).
MaxKeep the maximum value.
SumAccumulate numeric values.
CountIncrement by 1 for each occurrence.
MinKeep the minimum value.
UniqueCountTrack unique values and store the count.
TransformDescription
Base58EncodeEncode bytes to Base58 string (default for Pubkeys).
Base58DecodeDecode Base58 string to bytes.
HexEncodeEncode bytes to Hex string.
HexDecodeDecode Hex string to bytes.
ToStringConvert value to string.
ToNumberConvert value to number.

These methods are available in #[computed] expressions when using the TokenMetadata resolver. See Resolvers for details.

MethodDescription
TokenMetadata::ui_amount(raw, decimals)Convert raw token amount to human-readable UI amount.
TokenMetadata::raw_amount(ui, decimals)Convert UI amount back to raw token amount.