Skip to main content

hyperstack_interpreter/
resolvers.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::OnceLock;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// Context provided to primary key resolver functions
8pub struct ResolveContext<'a> {
9    #[allow(dead_code)]
10    pub(crate) state_id: u32,
11    pub(crate) slot: u64,
12    pub(crate) signature: String,
13    pub(crate) reverse_lookups:
14        &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
15}
16
17impl<'a> ResolveContext<'a> {
18    /// Create a new ResolveContext (primarily for use by generated code)
19    pub fn new(
20        state_id: u32,
21        slot: u64,
22        signature: String,
23        reverse_lookups: &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
24    ) -> Self {
25        Self {
26            state_id,
27            slot,
28            signature,
29            reverse_lookups,
30        }
31    }
32
33    /// Try to reverse lookup a PDA address to find the seed value
34    /// This is typically used to find the primary key from a PDA account address
35    pub fn pda_reverse_lookup(&mut self, pda_address: &str) -> Option<String> {
36        let lookup_name = "default_pda_lookup";
37        self.reverse_lookups
38            .get_mut(lookup_name)
39            .and_then(|t| t.lookup(pda_address))
40    }
41
42    pub fn slot(&self) -> u64 {
43        self.slot
44    }
45
46    pub fn signature(&self) -> &str {
47        &self.signature
48    }
49}
50
51/// Result of attempting to resolve a primary key
52pub enum KeyResolution {
53    /// Primary key successfully resolved
54    Found(String),
55
56    /// Queue this update until we see one of these instruction discriminators
57    /// The discriminators identify which instructions can populate the reverse lookup
58    QueueUntil(&'static [u8]),
59
60    /// Skip this update entirely (don't queue)
61    Skip,
62}
63
64/// Context provided to instruction hook functions
65pub struct InstructionContext<'a> {
66    pub(crate) accounts: HashMap<String, String>,
67    #[allow(dead_code)]
68    pub(crate) state_id: u32,
69    pub(crate) reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
70    pub(crate) pending_updates: Vec<crate::vm::PendingAccountUpdate>,
71    pub(crate) registers: Option<&'a mut Vec<crate::vm::RegisterValue>>,
72    pub(crate) state_reg: Option<crate::vm::Register>,
73    #[allow(dead_code)]
74    pub(crate) compiled_paths: Option<&'a HashMap<String, crate::metrics_context::CompiledPath>>,
75    pub(crate) instruction_data: Option<&'a serde_json::Value>,
76    pub(crate) slot: Option<u64>,
77    pub(crate) signature: Option<String>,
78    pub(crate) timestamp: Option<i64>,
79    pub(crate) dirty_tracker: crate::vm::DirtyTracker,
80}
81
82pub trait ReverseLookupUpdater {
83    fn update(
84        &mut self,
85        pda_address: String,
86        seed_value: String,
87    ) -> Vec<crate::vm::PendingAccountUpdate>;
88    fn flush_pending(&mut self, pda_address: &str) -> Vec<crate::vm::PendingAccountUpdate>;
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct TokenMetadata {
93    pub mint: String,
94    pub name: Option<String>,
95    pub symbol: Option<String>,
96    pub decimals: Option<u8>,
97    pub logo_uri: Option<String>,
98}
99
100#[derive(Debug, Clone, Copy)]
101pub struct ResolverTypeScriptSchema {
102    pub name: &'static str,
103    pub definition: &'static str,
104}
105
106#[derive(Debug, Clone, Copy)]
107pub struct ResolverComputedMethod {
108    pub name: &'static str,
109    pub arg_count: usize,
110}
111
112pub trait ResolverDefinition: Send + Sync {
113    fn name(&self) -> &'static str;
114    fn output_type(&self) -> &'static str;
115    fn computed_methods(&self) -> &'static [ResolverComputedMethod];
116    fn evaluate_computed(
117        &self,
118        method: &str,
119        args: &[Value],
120    ) -> std::result::Result<Value, Box<dyn std::error::Error>>;
121    fn typescript_interface(&self) -> Option<&'static str> {
122        None
123    }
124    fn typescript_schema(&self) -> Option<ResolverTypeScriptSchema> {
125        None
126    }
127}
128
129pub struct ResolverRegistry {
130    resolvers: HashMap<String, Box<dyn ResolverDefinition>>,
131}
132
133impl Default for ResolverRegistry {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl ResolverRegistry {
140    pub fn new() -> Self {
141        Self {
142            resolvers: HashMap::new(),
143        }
144    }
145
146    pub fn register(&mut self, resolver: Box<dyn ResolverDefinition>) {
147        self.resolvers.insert(resolver.name().to_string(), resolver);
148    }
149
150    pub fn resolver(&self, name: &str) -> Option<&dyn ResolverDefinition> {
151        self.resolvers.get(name).map(|resolver| resolver.as_ref())
152    }
153
154    pub fn definitions(&self) -> impl Iterator<Item = &dyn ResolverDefinition> {
155        self.resolvers.values().map(|resolver| resolver.as_ref())
156    }
157
158    pub fn is_output_type(&self, type_name: &str) -> bool {
159        self.resolvers
160            .values()
161            .any(|resolver| resolver.output_type() == type_name)
162    }
163
164    pub fn evaluate_computed(
165        &self,
166        resolver: &str,
167        method: &str,
168        args: &[Value],
169    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
170        let resolver_impl = self
171            .resolver(resolver)
172            .ok_or_else(|| format!("Unknown resolver '{}'", resolver))?;
173
174        let method_spec = resolver_impl
175            .computed_methods()
176            .iter()
177            .find(|spec| spec.name == method)
178            .ok_or_else(|| {
179                format!(
180                    "Resolver '{}' does not provide method '{}'",
181                    resolver, method
182                )
183            })?;
184
185        if method_spec.arg_count != args.len() {
186            return Err(format!(
187                "Resolver '{}' method '{}' expects {} args, got {}",
188                resolver,
189                method,
190                method_spec.arg_count,
191                args.len()
192            )
193            .into());
194        }
195
196        resolver_impl.evaluate_computed(method, args)
197    }
198
199    pub fn validate_computed_expr(
200        &self,
201        expr: &crate::ast::ComputedExpr,
202        errors: &mut Vec<String>,
203    ) {
204        match expr {
205            crate::ast::ComputedExpr::ResolverComputed {
206                resolver,
207                method,
208                args,
209            } => {
210                let resolver_impl = self.resolver(resolver);
211                if resolver_impl.is_none() {
212                    errors.push(format!("Unknown resolver '{}'", resolver));
213                } else if let Some(resolver_impl) = resolver_impl {
214                    let method_spec = resolver_impl
215                        .computed_methods()
216                        .iter()
217                        .find(|spec| spec.name == method);
218                    if let Some(method_spec) = method_spec {
219                        if method_spec.arg_count != args.len() {
220                            errors.push(format!(
221                                "Resolver '{}' method '{}' expects {} args, got {}",
222                                resolver,
223                                method,
224                                method_spec.arg_count,
225                                args.len()
226                            ));
227                        }
228                    } else {
229                        errors.push(format!(
230                            "Resolver '{}' does not provide method '{}'",
231                            resolver, method
232                        ));
233                    }
234                }
235
236                for arg in args {
237                    self.validate_computed_expr(arg, errors);
238                }
239            }
240            crate::ast::ComputedExpr::FieldRef { .. }
241            | crate::ast::ComputedExpr::Literal { .. }
242            | crate::ast::ComputedExpr::None
243            | crate::ast::ComputedExpr::Var { .. }
244            | crate::ast::ComputedExpr::ByteArray { .. }
245            | crate::ast::ComputedExpr::ContextSlot
246            | crate::ast::ComputedExpr::ContextTimestamp => {}
247            crate::ast::ComputedExpr::UnwrapOr { expr, .. }
248            | crate::ast::ComputedExpr::Cast { expr, .. }
249            | crate::ast::ComputedExpr::Paren { expr }
250            | crate::ast::ComputedExpr::Some { value: expr }
251            | crate::ast::ComputedExpr::Slice { expr, .. }
252            | crate::ast::ComputedExpr::Index { expr, .. }
253            | crate::ast::ComputedExpr::U64FromLeBytes { bytes: expr }
254            | crate::ast::ComputedExpr::U64FromBeBytes { bytes: expr }
255            | crate::ast::ComputedExpr::JsonToBytes { expr }
256            | crate::ast::ComputedExpr::Unary { expr, .. } => {
257                self.validate_computed_expr(expr, errors);
258            }
259            crate::ast::ComputedExpr::Binary { left, right, .. } => {
260                self.validate_computed_expr(left, errors);
261                self.validate_computed_expr(right, errors);
262            }
263            crate::ast::ComputedExpr::MethodCall { expr, args, .. } => {
264                self.validate_computed_expr(expr, errors);
265                for arg in args {
266                    self.validate_computed_expr(arg, errors);
267                }
268            }
269            crate::ast::ComputedExpr::Let { value, body, .. } => {
270                self.validate_computed_expr(value, errors);
271                self.validate_computed_expr(body, errors);
272            }
273            crate::ast::ComputedExpr::If {
274                condition,
275                then_branch,
276                else_branch,
277            } => {
278                self.validate_computed_expr(condition, errors);
279                self.validate_computed_expr(then_branch, errors);
280                self.validate_computed_expr(else_branch, errors);
281            }
282            crate::ast::ComputedExpr::Closure { body, .. } => {
283                self.validate_computed_expr(body, errors);
284            }
285        }
286    }
287}
288
289static BUILTIN_RESOLVER_REGISTRY: OnceLock<ResolverRegistry> = OnceLock::new();
290
291pub fn register_builtin_resolvers(registry: &mut ResolverRegistry) {
292    registry.register(Box::new(TokenMetadataResolver));
293}
294
295pub fn builtin_resolver_registry() -> &'static ResolverRegistry {
296    BUILTIN_RESOLVER_REGISTRY.get_or_init(|| {
297        let mut registry = ResolverRegistry::new();
298        register_builtin_resolvers(&mut registry);
299        registry
300    })
301}
302
303pub fn evaluate_resolver_computed(
304    resolver: &str,
305    method: &str,
306    args: &[Value],
307) -> std::result::Result<Value, Box<dyn std::error::Error>> {
308    builtin_resolver_registry().evaluate_computed(resolver, method, args)
309}
310
311pub fn validate_resolver_computed_specs(
312    specs: &[crate::ast::ComputedFieldSpec],
313) -> std::result::Result<(), Box<dyn std::error::Error>> {
314    let registry = builtin_resolver_registry();
315    let mut errors = Vec::new();
316
317    for spec in specs {
318        registry.validate_computed_expr(&spec.expression, &mut errors);
319    }
320
321    if errors.is_empty() {
322        Ok(())
323    } else {
324        Err(errors.join("\n").into())
325    }
326}
327
328pub fn is_resolver_output_type(type_name: &str) -> bool {
329    builtin_resolver_registry().is_output_type(type_name)
330}
331
332const DEFAULT_DAS_BATCH_SIZE: usize = 100;
333const DEFAULT_DAS_TIMEOUT_SECS: u64 = 10;
334const DAS_API_ENDPOINT_ENV: &str = "DAS_API_ENDPOINT";
335const DAS_API_BATCH_ENV: &str = "DAS_API_BATCH_SIZE";
336
337pub struct TokenMetadataResolverClient {
338    endpoint: String,
339    client: reqwest::Client,
340    batch_size: usize,
341}
342
343impl TokenMetadataResolverClient {
344    pub fn new(
345        endpoint: String,
346        batch_size: usize,
347    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
348        let client = reqwest::Client::builder()
349            .timeout(std::time::Duration::from_secs(DEFAULT_DAS_TIMEOUT_SECS))
350            .build()?;
351
352        Ok(Self {
353            endpoint,
354            client,
355            batch_size: batch_size.max(1),
356        })
357    }
358
359    pub fn from_env(
360    ) -> Result<Option<Self>, Box<dyn std::error::Error + Send + Sync>> {
361        let Some(endpoint) = std::env::var(DAS_API_ENDPOINT_ENV).ok() else {
362            return Ok(None);
363        };
364
365        let batch_size = std::env::var(DAS_API_BATCH_ENV)
366            .ok()
367            .and_then(|value| value.parse::<usize>().ok())
368            .unwrap_or(DEFAULT_DAS_BATCH_SIZE);
369
370        Ok(Some(Self::new(endpoint, batch_size)?))
371    }
372
373    pub async fn resolve_token_metadata(
374        &self,
375        mints: &[String],
376    ) -> Result<HashMap<String, Value>, Box<dyn std::error::Error + Send + Sync>> {
377        let mut unique = HashSet::new();
378        let mut deduped = Vec::new();
379
380        for mint in mints {
381            if mint.is_empty() {
382                continue;
383            }
384            if unique.insert(mint.clone()) {
385                deduped.push(mint.clone());
386            }
387        }
388
389        let mut results = HashMap::new();
390        if deduped.is_empty() {
391            return Ok(results);
392        }
393
394        for chunk in deduped.chunks(self.batch_size) {
395            let assets = self.fetch_assets(chunk).await?;
396            for asset in assets {
397                if let Some((mint, value)) = Self::build_token_metadata(&asset) {
398                    results.insert(mint, value);
399                }
400            }
401        }
402
403        Ok(results)
404    }
405
406    async fn fetch_assets(
407        &self,
408        ids: &[String],
409    ) -> Result<Vec<Value>, Box<dyn std::error::Error + Send + Sync>> {
410        let payload = serde_json::json!({
411            "jsonrpc": "2.0",
412            "id": "1",
413            "method": "getAssetBatch",
414            "params": {
415                "ids": ids,
416                "options": {
417                    "showFungible": true,
418                },
419            },
420        });
421
422        let response = self.client.post(&self.endpoint).json(&payload).send().await?;
423        let response = response.error_for_status()?;
424        let value = response.json::<Value>().await?;
425
426        if let Some(error) = value.get("error") {
427            return Err(format!("Resolver response error: {}", error).into());
428        }
429
430        let assets = value
431            .get("result")
432            .and_then(|result| match result {
433                Value::Array(items) => Some(items.clone()),
434                Value::Object(obj) => obj
435                    .get("items")
436                    .and_then(|items| items.as_array())
437                    .cloned(),
438                _ => None,
439            })
440            .ok_or_else(|| "Resolver response missing result".to_string())?;
441
442        let assets = assets.into_iter().filter(|a| !a.is_null()).collect();
443        Ok(assets)
444    }
445
446    fn build_token_metadata(asset: &Value) -> Option<(String, Value)> {
447        let mint = asset.get("id").and_then(|value| value.as_str())?.to_string();
448
449        let name = asset
450            .pointer("/content/metadata/name")
451            .and_then(|value| value.as_str());
452
453        let symbol = asset
454            .pointer("/content/metadata/symbol")
455            .and_then(|value| value.as_str());
456
457        let token_info = asset.get("token_info").or_else(|| asset.pointer("/content/token_info"));
458
459        let decimals = token_info
460            .and_then(|info| info.get("decimals"))
461            .and_then(|value| value.as_u64());
462
463        let logo_uri = asset
464            .pointer("/content/links/image")
465            .and_then(|value| value.as_str())
466            .or_else(|| asset.pointer("/content/links/image_uri").and_then(|value| value.as_str()));
467
468        let mut obj = serde_json::Map::new();
469        obj.insert("mint".to_string(), serde_json::json!(mint));
470        obj.insert(
471            "name".to_string(),
472            name.map(|value| serde_json::json!(value))
473                .unwrap_or(Value::Null),
474        );
475        obj.insert(
476            "symbol".to_string(),
477            symbol.map(|value| serde_json::json!(value))
478                .unwrap_or(Value::Null),
479        );
480        obj.insert(
481            "decimals".to_string(),
482            decimals
483                .map(|value| serde_json::json!(value))
484                .unwrap_or(Value::Null),
485        );
486        obj.insert(
487            "logo_uri".to_string(),
488            logo_uri
489                .map(|value| serde_json::json!(value))
490                .unwrap_or(Value::Null),
491        );
492
493        Some((mint, Value::Object(obj)))
494    }
495}
496
497struct TokenMetadataResolver;
498
499const TOKEN_METADATA_METHODS: &[ResolverComputedMethod] = &[
500    ResolverComputedMethod {
501        name: "ui_amount",
502        arg_count: 2,
503    },
504    ResolverComputedMethod {
505        name: "raw_amount",
506        arg_count: 2,
507    },
508];
509
510impl TokenMetadataResolver {
511    fn optional_f64(value: &Value) -> Option<f64> {
512        if value.is_null() {
513            return None;
514        }
515        match value {
516            Value::Number(number) => number.as_f64(),
517            Value::String(text) => text.parse::<f64>().ok(),
518            _ => None,
519        }
520    }
521
522    fn optional_u8(value: &Value) -> Option<u8> {
523        if value.is_null() {
524            return None;
525        }
526        match value {
527            Value::Number(number) => number
528                .as_u64()
529                .or_else(|| {
530                    number
531                        .as_i64()
532                        .and_then(|v| if v >= 0 { Some(v as u64) } else { None })
533                })
534                .and_then(|v| u8::try_from(v).ok()),
535            Value::String(text) => text.parse::<u8>().ok(),
536            _ => None,
537        }
538    }
539
540    fn evaluate_ui_amount(
541        args: &[Value],
542    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
543        let raw_value = Self::optional_f64(&args[0]);
544        let decimals = Self::optional_u8(&args[1]);
545
546        match (raw_value, decimals) {
547            (Some(value), Some(decimals)) => {
548                let factor = 10_f64.powi(decimals as i32);
549                let result = value / factor;
550                if result.is_finite() {
551                    serde_json::Number::from_f64(result)
552                        .map(Value::Number)
553                        .ok_or_else(|| "Failed to serialize ui_amount".into())
554                } else {
555                    Err("ui_amount result is not finite".into())
556                }
557            }
558            _ => Ok(Value::Null),
559        }
560    }
561
562    fn evaluate_raw_amount(
563        args: &[Value],
564    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
565        let ui_value = Self::optional_f64(&args[0]);
566        let decimals = Self::optional_u8(&args[1]);
567
568        match (ui_value, decimals) {
569            (Some(value), Some(decimals)) => {
570                let factor = 10_f64.powi(decimals as i32);
571                let result = value * factor;
572                if !result.is_finite() || result < 0.0 {
573                    return Err("raw_amount result is not finite".into());
574                }
575                let rounded = result.round();
576                if rounded > u64::MAX as f64 {
577                    return Err("raw_amount result exceeds u64".into());
578                }
579                Ok(Value::Number(serde_json::Number::from(rounded as u64)))
580            }
581            _ => Ok(Value::Null),
582        }
583    }
584}
585
586impl ResolverDefinition for TokenMetadataResolver {
587    fn name(&self) -> &'static str {
588        "TokenMetadata"
589    }
590
591    fn output_type(&self) -> &'static str {
592        "TokenMetadata"
593    }
594
595    fn computed_methods(&self) -> &'static [ResolverComputedMethod] {
596        TOKEN_METADATA_METHODS
597    }
598
599    fn evaluate_computed(
600        &self,
601        method: &str,
602        args: &[Value],
603    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
604        match method {
605            "ui_amount" => Self::evaluate_ui_amount(args),
606            "raw_amount" => Self::evaluate_raw_amount(args),
607            _ => Err(format!("Unknown TokenMetadata method '{}'", method).into()),
608        }
609    }
610
611    fn typescript_interface(&self) -> Option<&'static str> {
612        Some(
613            r#"export interface TokenMetadata {
614  mint: string;
615  name?: string | null;
616  symbol?: string | null;
617  decimals?: number | null;
618  logo_uri?: string | null;
619}"#,
620        )
621    }
622
623    fn typescript_schema(&self) -> Option<ResolverTypeScriptSchema> {
624        Some(ResolverTypeScriptSchema {
625            name: "TokenMetadataSchema",
626            definition: r#"export const TokenMetadataSchema = z.object({
627  mint: z.string(),
628  name: z.string().nullable().optional(),
629  symbol: z.string().nullable().optional(),
630  decimals: z.number().nullable().optional(),
631  logo_uri: z.string().nullable().optional(),
632});"#,
633        })
634    }
635}
636
637impl<'a> InstructionContext<'a> {
638    pub fn new(
639        accounts: HashMap<String, String>,
640        state_id: u32,
641        reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
642    ) -> Self {
643        Self {
644            accounts,
645            state_id,
646            reverse_lookup_tx,
647            pending_updates: Vec::new(),
648            registers: None,
649            state_reg: None,
650            compiled_paths: None,
651            instruction_data: None,
652            slot: None,
653            signature: None,
654            timestamp: None,
655            dirty_tracker: crate::vm::DirtyTracker::new(),
656        }
657    }
658
659    #[allow(clippy::too_many_arguments)]
660    pub fn with_metrics(
661        accounts: HashMap<String, String>,
662        state_id: u32,
663        reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
664        registers: &'a mut Vec<crate::vm::RegisterValue>,
665        state_reg: crate::vm::Register,
666        compiled_paths: &'a HashMap<String, crate::metrics_context::CompiledPath>,
667        instruction_data: &'a serde_json::Value,
668        slot: Option<u64>,
669        signature: Option<String>,
670        timestamp: i64,
671    ) -> Self {
672        Self {
673            accounts,
674            state_id,
675            reverse_lookup_tx,
676            pending_updates: Vec::new(),
677            registers: Some(registers),
678            state_reg: Some(state_reg),
679            compiled_paths: Some(compiled_paths),
680            instruction_data: Some(instruction_data),
681            slot,
682            signature,
683            timestamp: Some(timestamp),
684            dirty_tracker: crate::vm::DirtyTracker::new(),
685        }
686    }
687
688    /// Get an account address by its name from the instruction
689    pub fn account(&self, name: &str) -> Option<String> {
690        self.accounts.get(name).cloned()
691    }
692
693    /// Register a reverse lookup: PDA address -> seed value
694    /// This also flushes any pending account updates for this PDA
695    ///
696    /// The pending account updates are accumulated internally and can be retrieved
697    /// via `take_pending_updates()` after all hooks have executed.
698    pub fn register_pda_reverse_lookup(&mut self, pda_address: &str, seed_value: &str) {
699        let pending = self
700            .reverse_lookup_tx
701            .update(pda_address.to_string(), seed_value.to_string());
702        self.pending_updates.extend(pending);
703    }
704
705    /// Take all accumulated pending updates
706    ///
707    /// This should be called after all instruction hooks have executed to retrieve
708    /// any pending account updates that need to be reprocessed.
709    pub fn take_pending_updates(&mut self) -> Vec<crate::vm::PendingAccountUpdate> {
710        std::mem::take(&mut self.pending_updates)
711    }
712
713    pub fn dirty_tracker(&self) -> &crate::vm::DirtyTracker {
714        &self.dirty_tracker
715    }
716
717    pub fn dirty_tracker_mut(&mut self) -> &mut crate::vm::DirtyTracker {
718        &mut self.dirty_tracker
719    }
720
721    /// Get the current state register value (for generating mutations)
722    pub fn state_value(&self) -> Option<&serde_json::Value> {
723        if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
724            Some(&registers[state_reg])
725        } else {
726            None
727        }
728    }
729
730    /// Get a field value from the entity state
731    /// This allows reading aggregated values or other entity fields
732    pub fn get<T: serde::de::DeserializeOwned>(&self, field_path: &str) -> Option<T> {
733        if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
734            let state = &registers[state_reg];
735            self.get_nested_value(state, field_path)
736                .and_then(|v| serde_json::from_value(v.clone()).ok())
737        } else {
738            None
739        }
740    }
741
742    pub fn set<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
743        if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
744            let serialized = serde_json::to_value(value).ok();
745            if let Some(val) = serialized {
746                Self::set_nested_value_static(&mut registers[state_reg], field_path, val);
747                self.dirty_tracker.mark_replaced(field_path);
748                println!("      ✓ Set field '{}' and marked as dirty", field_path);
749            }
750        } else {
751            println!("      ⚠️  Cannot set field '{}': metrics not configured (registers={}, state_reg={:?})", 
752                field_path, self.registers.is_some(), self.state_reg);
753        }
754    }
755
756    pub fn increment(&mut self, field_path: &str, amount: i64) {
757        let current = self.get::<i64>(field_path).unwrap_or(0);
758        self.set(field_path, current + amount);
759    }
760
761    pub fn append<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
762        if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
763            let serialized = serde_json::to_value(&value).ok();
764            if let Some(val) = serialized {
765                Self::append_to_array_static(&mut registers[state_reg], field_path, val.clone());
766                self.dirty_tracker.mark_appended(field_path, val);
767                println!(
768                    "      ✓ Appended to '{}' and marked as appended",
769                    field_path
770                );
771            }
772        } else {
773            println!(
774                "      ⚠️  Cannot append to '{}': metrics not configured",
775                field_path
776            );
777        }
778    }
779
780    fn append_to_array_static(
781        value: &mut serde_json::Value,
782        path: &str,
783        new_value: serde_json::Value,
784    ) {
785        let segments: Vec<&str> = path.split('.').collect();
786        if segments.is_empty() {
787            return;
788        }
789
790        let mut current = value;
791        for segment in &segments[..segments.len() - 1] {
792            if !current.is_object() {
793                *current = serde_json::json!({});
794            }
795            let obj = current.as_object_mut().unwrap();
796            current = obj
797                .entry(segment.to_string())
798                .or_insert(serde_json::json!({}));
799        }
800
801        let last_segment = segments[segments.len() - 1];
802        if !current.is_object() {
803            *current = serde_json::json!({});
804        }
805        let obj = current.as_object_mut().unwrap();
806        let arr = obj
807            .entry(last_segment.to_string())
808            .or_insert_with(|| serde_json::json!([]));
809        if let Some(arr) = arr.as_array_mut() {
810            arr.push(new_value);
811        }
812    }
813
814    fn get_nested_value<'b>(
815        &self,
816        value: &'b serde_json::Value,
817        path: &str,
818    ) -> Option<&'b serde_json::Value> {
819        let mut current = value;
820        for segment in path.split('.') {
821            current = current.get(segment)?;
822        }
823        Some(current)
824    }
825
826    fn set_nested_value_static(
827        value: &mut serde_json::Value,
828        path: &str,
829        new_value: serde_json::Value,
830    ) {
831        let segments: Vec<&str> = path.split('.').collect();
832        if segments.is_empty() {
833            return;
834        }
835
836        let mut current = value;
837        for segment in &segments[..segments.len() - 1] {
838            if !current.is_object() {
839                *current = serde_json::json!({});
840            }
841            let obj = current.as_object_mut().unwrap();
842            current = obj
843                .entry(segment.to_string())
844                .or_insert(serde_json::json!({}));
845        }
846
847        if !current.is_object() {
848            *current = serde_json::json!({});
849        }
850        if let Some(obj) = current.as_object_mut() {
851            obj.insert(segments[segments.len() - 1].to_string(), new_value);
852        }
853    }
854
855    /// Access instruction data field
856    pub fn data<T: serde::de::DeserializeOwned>(&self, field: &str) -> Option<T> {
857        self.instruction_data
858            .and_then(|data| data.get(field))
859            .and_then(|v| serde_json::from_value(v.clone()).ok())
860    }
861
862    /// Get the current timestamp
863    pub fn timestamp(&self) -> i64 {
864        self.timestamp.unwrap_or(0)
865    }
866
867    /// Get the current slot
868    pub fn slot(&self) -> Option<u64> {
869        self.slot
870    }
871
872    /// Get the current signature
873    pub fn signature(&self) -> Option<&str> {
874        self.signature.as_deref()
875    }
876}