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.
Population strategies define how an entity’s state is updated when new data arrives from the blockchain. Choosing the right strategy is critical for ensuring your projected state accurately reflects the underlying on-chain activity.
What are Population Strategies?
When a Hyperstack handler processes a transaction or account update, it maps data from the source (e.g., an Anchor instruction or account field) into your entity’s fields. A Population Strategy determines how that incoming value interacts with the existing value in that field.
For example, should a “Total Volume” field be overwritten by the latest trade amount, or should the latest amount be added to the current total? Strategies answer this question.
Strategy Selection Guide
Use this decision tree to identify the correct strategy for your field:
Quick Reference Table
| Strategy | Behavior | Best For |
|---|
LastWrite | Overwrites with newest data (Default) | Balances, Status, Current Prices |
SetOnce | Only sets if field is empty | IDs, Creation Timestamps, Owners |
Sum | Adds incoming value to current | Volume, Total Rewards, TVL |
Count | Increments by 1 | Trade Count, User Count, Event Count |
Append | Adds to an array/list | Trade History, Activity Logs |
Max / Min | Tracks the peak/trough | High/Low Prices, Peak Liquidity |
UniqueCount | Counts distinct occurrences | Active Users, Unique Voters |
Merge | Merges keys in an object | Configuration, Metadata |
Detailed Reference
LastWrite (Default)
The most common strategy. Whenever a new value arrives, it completely replaces the previous one.
- Use Case: Tracking the current state of an account
- Example:
#[map(ore_sdk::accounts::Round::total_deployed, strategy = LastWrite)]
pub total_deployed: Option<u64>,
Real example from ORE stack:
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundState {
#[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>,
#[map(ore_sdk::accounts::Round::total_miners, strategy = LastWrite)]
pub total_miners: Option<u64>,
}
SetOnce
The field is populated only once. Subsequent updates for the same entity will ignore this mapping if the field already has a value.
- Use Case: Immutable properties or “First Seen” metadata
- Example:
#[map(ore_sdk::accounts::Round::id, primary_key, strategy = SetOnce)]
pub round_id: u64,
Real example from ORE stack:
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundId {
#[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,
}
Sum
Numeric values are added to the existing value. This is the foundation of tracking volume and throughput.
- Use Case: Financial metrics and cumulative totals
- Example:
#[aggregate(from = ore_sdk::instructions::Deploy, field = args::amount, strategy = Sum, lookup_by = accounts::round)]
pub total_deployed_amount: Option<u64>,
How it works:
- Initial state:
total_deployed_amount = 0
- Deploy with
amount = 1000 → total_deployed_amount = 1000
- Deploy with
amount = 500 → total_deployed_amount = 1500
- Deploy with
amount = 2000 → total_deployed_amount = 3500
Count
Ignores the incoming value and simply increments the field by 1 for every match.
- Use Case: Tracking throughput or frequency
- Example:
#[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
pub deploy_count: Option<u64>,
Real example from ORE stack:
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundMetrics {
// Count of deploy instructions for this round
#[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
pub deploy_count: Option<u64>,
// Count of checkpoint instructions for this round
#[aggregate(from = ore_sdk::instructions::Checkpoint, strategy = Count, lookup_by = accounts::round)]
pub checkpoint_count: Option<u64>,
}
Append
Adds the incoming value to a list. Use this to maintain a linear history of events within an entity.
- Use Case: Event logs, audit trails
- Example:
#[event(from = Liquidate, strategy = Append)]
pub liquidation_history: Vec<LiquidationEvent>,
Warning: Append grows the size of your entity state. Avoid appending thousands of items to a single entity if you only need the latest state.
Max / Min
Keeps the highest or lowest value seen across all updates.
- Use Case: 24h Highs/Lows, price discovery
- Example:
#[aggregate(from = OracleUpdate, field = price, strategy = Max)]
pub all_time_high: u64,
UniqueCount
Maintains a set of unique values internally but projects only the count of that set.
- Use Case: Tracking “Active Users” or unique participants
- Example:
#[aggregate(from = Vote, field = accounts::voter, strategy = UniqueCount)]
pub total_unique_voters: u32,
Merge
Used for object-like fields where you want to update specific keys without overwriting the entire object.
- Use Case: Dynamic configuration maps
- Example:
#[map(from = Config, strategy = Merge)]
pub metadata: Map<String, String>,
Common Patterns
Token Price Tracking
| Field | Strategy | Why |
|---|
current_price | LastWrite | You only care about the most recent oracle update |
day_high | Max | Tracks the peak price seen in the stream |
daily_volume | Sum | Every swap adds to the total |
Governance Tracking
| Field | Strategy | Why |
|---|
total_votes | Sum | Sum of vote weights |
voter_count | UniqueCount | Count unique public keys that voted |
first_vote_at | SetOnce | Record when the first vote was cast |
Mining Round Tracking (ORE)
| Field | Strategy | Why |
|---|
round_id | SetOnce | ID never changes |
total_deployed | LastWrite | Account stores current total |
deploy_count | Count | Count each Deploy instruction |
motherlode | LastWrite | Prize pool updated by program |
Real Example: Complete Entity
From the ORE stack, combining multiple strategies:
#[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 {
// SetOnce: ID never changes
#[map(ore_sdk::accounts::Round::id, primary_key, strategy = SetOnce)]
pub round_id: u64,
// SetOnce: Address never changes
#[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: Track current value
#[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite)]
pub motherlode: Option<u64>,
// LastWrite: Track current value
#[map(ore_sdk::accounts::Round::total_deployed, strategy = LastWrite)]
pub total_deployed: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundMetrics {
// Count: Increment for each Deploy instruction
#[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
pub deploy_count: Option<u64>,
// Count: Increment for each Checkpoint instruction
#[aggregate(from = ore_sdk::instructions::Checkpoint, strategy = Count, lookup_by = accounts::round)]
pub checkpoint_count: Option<u64>,
}
Common Mistakes to Avoid
1. Using LastWrite for Volume
If you use LastWrite for a volume field, it will only show the amount of the most recent trade, not the total.
❌ Wrong:
#[map(Trade::amount, strategy = LastWrite)] // Only shows last trade!
pub total_volume: u64,
✅ Correct:
#[aggregate(from = Trade, field = args::amount, strategy = Sum)]
pub total_volume: u64,
2. Forgetting SetOnce for IDs
If you use LastWrite for an ID field, it might be overwritten unexpectedly.
❌ Wrong:
#[map(Round::id, primary_key, strategy = LastWrite)] // ID could change!
pub round_id: u64,
✅ Correct:
#[map(Round::id, primary_key, strategy = SetOnce)]
pub round_id: u64,
3. Appending Unnecessarily
Append grows entity state linearly. For high-frequency events, this can become a problem.
❌ Wrong:
#[event(from = Trade, fields = [amount, price], strategy = Append)] // Could be 1000s of trades!
pub all_trades: Vec<Trade>,
✅ Better:
// Track aggregates instead
#[aggregate(from = Trade, strategy = Count)]
pub trade_count: u64,
#[aggregate(from = Trade, field = args::amount, strategy = Sum)]
pub total_volume: u64,
4. Not Specifying Strategy for Aggregates
Always explicitly state the strategy for aggregations to ensure clarity.
❌ Wrong:
#[aggregate(from = Deploy)] // What should this do?
pub deploys: ???,
✅ Correct:
#[aggregate(from = Deploy, strategy = Count)]
pub deploy_count: u64,
Next Steps