Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ff65ed0
fix(macros): prevent cross-entity instruction hook contamination
adiman9 Mar 11, 2026
04486af
feat(idl): add packed representation support to IdlRepr
adiman9 Mar 13, 2026
178cffc
chore: update dependencies in ore stack and examples
adiman9 Mar 13, 2026
cd856fa
feat(macros): add CPI event support with camelCase field handling
adiman9 Mar 13, 2026
b337d09
feat(macros): support packed structs, large arrays, and stream spec i…
adiman9 Mar 13, 2026
d8b46ea
feat(macros): enhance Vixen runtime with improved tracing and queue h…
adiman9 Mar 13, 2026
23503ac
feat(interpreter): various compiler and VM improvements
adiman9 Mar 13, 2026
dc0eb5e
feat: update hyperstack lib exports
adiman9 Mar 13, 2026
acd5a31
fix: resolve compilation errors in hyperstack-macros
adiman9 Mar 14, 2026
3fee1b1
fix: silence unused variable warning in stream_spec
adiman9 Mar 14, 2026
a8a62cf
fix: track only actually emitted enum types to prevent over-eager ded…
adiman9 Mar 14, 2026
d338399
fix: remove redundant is_cpi_event variable shadowing
adiman9 Mar 14, 2026
0bdd7d4
fix: track only actually emitted enum types to prevent over-eager ded…
adiman9 Mar 14, 2026
cdb0d39
fix: panic on missing event type definition instead of silent empty s…
adiman9 Mar 14, 2026
c101bc5
fix: reject oversized sequences in big_array deserializer
adiman9 Mar 14, 2026
f8a5223
fix: address code review issues in interpreter VM
adiman9 Mar 14, 2026
258325f
fix: add event type definitions to ore IDL
adiman9 Mar 14, 2026
d745423
chore: Update stack
adiman9 Mar 14, 2026
908c9ff
fix: address code review issues in macro codegen
adiman9 Mar 14, 2026
4e43b39
chore: Release please config formatting
adiman9 Mar 14, 2026
c9fb961
fix(interpreter): zero-variant enum dedup guard escape
adiman9 Mar 14, 2026
c1057e0
refactor(macros): extract shared resolve_snapshot_source helper
adiman9 Mar 14, 2026
d626ac1
docs(macros): clarify is_large_array only checks outer dimension
adiman9 Mar 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions examples/ore-server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions hyperstack-idl/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ pub enum IdlTypeDefinedInner {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct IdlRepr {
pub kind: String,
#[serde(default)]
pub packed: Option<bool>,
}

/// Account serialization format as specified in the IDL.
Expand Down
35 changes: 31 additions & 4 deletions hyperstack-macros/src/ast/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ pub fn build_handlers_from_sources(
for ((source_type, join_key), mappings) in &sources_by_type_and_join {
let account_type = source_type.split("::").last().unwrap_or(source_type);
let is_instruction = mappings.iter().any(|m| m.is_instruction);
// CPI events are sourced from `::events::` submodule paths.
// All event fields live under "data.*" (no "accounts.*" section).
let is_cpi_event = source_type.contains("::events::");

// Skip if this is an event-derived mapping
if is_instruction
Expand Down Expand Up @@ -404,7 +407,14 @@ pub fn build_handlers_from_sources(

MappingSource::AsCapture { field_transforms }
} else {
let field_path = if is_instruction {
let field_path = if is_cpi_event {
// CPI events: all fields are under "data"
if mapping.source_field_name.is_empty() {
FieldPath::new(&["data"])
} else {
FieldPath::new(&["data", &mapping.source_field_name])
}
} else if is_instruction {
if mapping.source_field_name.is_empty() {
FieldPath::new(&["data"])
} else {
Expand Down Expand Up @@ -474,7 +484,10 @@ pub fn build_handlers_from_sources(

if mapping.is_primary_key {
has_primary_key = true;
if is_instruction {
if is_cpi_event {
// CPI event fields are always in "data"
primary_field = Some(format!("data.{}", mapping.source_field_name));
} else if is_instruction {
let prefix = idl
.and_then(|idl| {
idl.get_instruction_field_prefix(
Expand Down Expand Up @@ -503,10 +516,17 @@ pub fn build_handlers_from_sources(
.find_map(|m| m.lookup_by.as_ref())
.map(|fs| {
// FieldSpec has explicit_location which tells us if it's accounts:: or data::
// For CPI events, all fields (including identifiers) are under "data".
let prefix = match &fs.explicit_location {
Some(parse::FieldLocation::Account) => "accounts",
Some(parse::FieldLocation::InstructionArg) => "data",
None => "accounts", // Default to accounts for compatibility
None => {
if is_cpi_event {
"data" // CPI event fields are always in "data"
} else {
"accounts" // Default to accounts for instruction compatibility
}
}
};
format!("{}.{}", prefix, fs.ident)
});
Expand Down Expand Up @@ -544,7 +564,14 @@ pub fn build_handlers_from_sources(
}
};

let type_suffix = if is_instruction { "IxState" } else { "State" };
// CPI events (from `::events::`) use "CpiEvent" suffix; instructions use "IxState"
let type_suffix = if is_cpi_event {
"CpiEvent"
} else if is_instruction {
"IxState"
} else {
"State"
};
let serialization = if is_instruction {
None
} else {
Expand Down
89 changes: 56 additions & 33 deletions hyperstack-macros/src/codegen/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,71 @@ pub fn generate_hook_actions(
actions: &[HookAction],
_lookup_by: &Option<FieldPath>,
) -> TokenStream {
let action_code: Vec<TokenStream> = actions.iter().map(|action| {
match action {
HookAction::RegisterPdaMapping { pda_field, seed_field, lookup_name: _ } => {
let pda_field_str = pda_field.segments.last().cloned().unwrap_or_default();
let seed_field_str = seed_field.segments.last().cloned().unwrap_or_default();

quote! {
if let (Some(pda), Some(seed)) = (ctx.account(#pda_field_str), ctx.account(#seed_field_str)) {
ctx.register_pda_reverse_lookup(&pda, &seed);
let action_code: Vec<TokenStream> = actions
.iter()
.map(|action| {
match action {
HookAction::RegisterPdaMapping {
pda_field,
seed_field,
lookup_name: _,
} => {
let pda_raw = pda_field.segments.last().cloned().unwrap_or_default();
let seed_raw = seed_field.segments.last().cloned().unwrap_or_default();
let pda_camel = crate::event_type_helpers::snake_to_lower_camel(&pda_raw);
let seed_camel = crate::event_type_helpers::snake_to_lower_camel(&seed_raw);

// IDL account names can be camelCase (e.g. Pumpfun: "bondingCurve") or
// snake_case (e.g. Raydium: "pool_state"). The register_from attribute
// uses Rust field names (always snake_case), so try both the camelCase
// conversion and the raw snake_case name to match the IDL.
quote! {
let pda_val = ctx.account(#pda_camel).or_else(|| ctx.account(#pda_raw));
let seed_val = ctx.account(#seed_camel).or_else(|| ctx.account(#seed_raw));
if let (Some(pda), Some(seed)) = (pda_val, seed_val) {
ctx.register_pda_reverse_lookup(&pda, &seed);
}
}
}
}
HookAction::SetField { target_field, source, condition } => {
let set_code = generate_set_field_code(target_field, source);
if let Some(cond) = condition {
let cond_code = generate_condition_code(cond);
quote! {
if #cond_code {
#set_code
HookAction::SetField {
target_field,
source,
condition,
} => {
let set_code = generate_set_field_code(target_field, source);
if let Some(cond) = condition {
let cond_code = generate_condition_code(cond);
quote! {
if #cond_code {
#set_code
}
}
} else {
set_code
}
} else {
set_code
}
}
HookAction::IncrementField { target_field, increment_by, condition } => {
let increment_code = quote! {
ctx.increment(#target_field, #increment_by);
};
if let Some(cond) = condition {
let cond_code = generate_condition_code(cond);
quote! {
if #cond_code {
#increment_code
HookAction::IncrementField {
target_field,
increment_by,
condition,
} => {
let increment_code = quote! {
ctx.increment(#target_field, #increment_by);
};
if let Some(cond) = condition {
let cond_code = generate_condition_code(cond);
quote! {
if #cond_code {
#increment_code
}
}
} else {
increment_code
}
} else {
increment_code
}
}
}
}).collect();
})
.collect();

quote! {
#(#action_code)*
Expand Down
34 changes: 31 additions & 3 deletions hyperstack-macros/src/codegen/vixen_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,12 @@ pub fn generate_vm_handler(
}
hyperstack::runtime::hyperstack_interpreter::resolvers::KeyResolution::QueueUntil(_discriminators) => {
let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner());
tracing::info!(
event_type = %event_type,
pda = %account_address,
slot = slot,
"QueueUntil: queueing account update for later flush"
);

let _ = vm.queue_account_update(
0,
Expand Down Expand Up @@ -898,6 +904,11 @@ pub fn generate_vm_handler(

// Process pending account updates from instruction hooks
if !pending_updates.is_empty() {
tracing::info!(
count = pending_updates.len(),
event_type = %event_type,
"Flushing pending account updates from instruction hooks"
);
for update in pending_updates {
let resolved_key = vm.try_pda_reverse_lookup(0, "default_pda_lookup", &update.pda_address);

Expand All @@ -916,11 +927,23 @@ pub fn generate_vm_handler(

match vm.process_event(&bytecode, account_data, &update.account_type, Some(&update_context), None) {
Ok(pending_mutations) => {
tracing::info!(
account_type = %update.account_type,
pda = %update.pda_address,
mutations = pending_mutations.len(),
"Reprocessed flushed account update"
);
if let Ok(ref mut mutations) = result {
mutations.extend(pending_mutations);
}
}
Err(_e) => {}
Err(e) => {
tracing::warn!(
account_type = %update.account_type,
error = %e,
"Failed to reprocess flushed account update"
);
}
}
}
}
Expand Down Expand Up @@ -1699,7 +1722,6 @@ pub fn generate_account_handler_impl(
}
hyperstack::runtime::hyperstack_interpreter::resolvers::KeyResolution::QueueUntil(_discriminators) => {
let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner());

let _ = vm.queue_account_update(
0,
hyperstack::runtime::hyperstack_interpreter::QueuedAccountUpdate {
Expand Down Expand Up @@ -1900,7 +1922,13 @@ pub fn generate_instruction_handler_impl(
mutations.extend(pending_mutations);
}
}
Err(_e) => {}
Err(e) => {
hyperstack::runtime::tracing::warn!(
account_type = %update.account_type,
error = %e,
"Flushed account reprocessing failed"
);
}
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions hyperstack-macros/src/event_type_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,23 @@ pub fn program_name_for_type<'a>(type_str: &str, idls: IdlLookup<'a>) -> Option<
pub fn program_name_from_sdk_prefix(sdk_module: &str) -> &str {
sdk_module.strip_suffix("_sdk").unwrap_or(sdk_module)
}

/// Convert a snake_case identifier to lowerCamelCase.
/// "bonding_curve" -> "bondingCurve"
/// "mint" -> "mint" (single word unchanged)
/// "associated_bonding_curve" -> "associatedBondingCurve"
pub fn snake_to_lower_camel(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = false;
for ch in s.chars() {
if ch == '_' {
capitalize_next = true;
} else if capitalize_next {
result.extend(ch.to_uppercase());
capitalize_next = false;
} else {
result.push(ch);
}
}
result
}
Loading
Loading