Skip to main content

hyperstack_interpreter/
resolvers.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::OnceLock;
3
4use futures::future::join_all;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// Context provided to primary key resolver functions
10pub struct ResolveContext<'a> {
11    #[allow(dead_code)]
12    pub(crate) state_id: u32,
13    pub(crate) slot: u64,
14    pub(crate) signature: String,
15    pub(crate) reverse_lookups:
16        &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
17}
18
19impl<'a> ResolveContext<'a> {
20    /// Create a new ResolveContext (primarily for use by generated code)
21    pub fn new(
22        state_id: u32,
23        slot: u64,
24        signature: String,
25        reverse_lookups: &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
26    ) -> Self {
27        Self {
28            state_id,
29            slot,
30            signature,
31            reverse_lookups,
32        }
33    }
34
35    /// Try to reverse lookup a PDA address to find the seed value
36    /// This is typically used to find the primary key from a PDA account address
37    pub fn pda_reverse_lookup(&mut self, pda_address: &str) -> Option<String> {
38        let lookup_name = "default_pda_lookup";
39        self.reverse_lookups
40            .get_mut(lookup_name)
41            .and_then(|t| t.lookup(pda_address))
42    }
43
44    pub fn slot(&self) -> u64 {
45        self.slot
46    }
47
48    pub fn signature(&self) -> &str {
49        &self.signature
50    }
51}
52
53/// Result of attempting to resolve a primary key
54pub enum KeyResolution {
55    /// Primary key successfully resolved
56    Found(String),
57
58    /// Queue this update until we see one of these instruction discriminators
59    /// The discriminators identify which instructions can populate the reverse lookup
60    QueueUntil(&'static [u8]),
61
62    /// Skip this update entirely (don't queue)
63    Skip,
64}
65
66/// Context provided to instruction hook functions
67pub struct InstructionContext<'a> {
68    pub(crate) accounts: HashMap<String, String>,
69    #[allow(dead_code)]
70    pub(crate) state_id: u32,
71    pub(crate) reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
72    pub(crate) pending_updates: Vec<crate::vm::PendingAccountUpdate>,
73    pub(crate) registers: Option<&'a mut Vec<crate::vm::RegisterValue>>,
74    pub(crate) state_reg: Option<crate::vm::Register>,
75    #[allow(dead_code)]
76    pub(crate) compiled_paths: Option<&'a HashMap<String, crate::metrics_context::CompiledPath>>,
77    pub(crate) instruction_data: Option<&'a serde_json::Value>,
78    pub(crate) slot: Option<u64>,
79    pub(crate) signature: Option<String>,
80    pub(crate) timestamp: Option<i64>,
81    pub(crate) dirty_tracker: crate::vm::DirtyTracker,
82}
83
84pub trait ReverseLookupUpdater {
85    fn update(
86        &mut self,
87        pda_address: String,
88        seed_value: String,
89    ) -> Vec<crate::vm::PendingAccountUpdate>;
90    fn flush_pending(&mut self, pda_address: &str) -> Vec<crate::vm::PendingAccountUpdate>;
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct TokenMetadata {
95    pub mint: String,
96    pub name: Option<String>,
97    pub symbol: Option<String>,
98    pub decimals: Option<u8>,
99    pub logo_uri: Option<String>,
100}
101
102#[derive(Debug, Clone, Copy)]
103pub struct ResolverTypeScriptSchema {
104    pub name: &'static str,
105    pub definition: &'static str,
106}
107
108#[derive(Debug, Clone, Copy)]
109pub struct ResolverComputedMethod {
110    pub name: &'static str,
111    pub arg_count: usize,
112}
113
114pub trait ResolverDefinition: Send + Sync {
115    fn name(&self) -> &'static str;
116    fn output_type(&self) -> &'static str;
117    fn computed_methods(&self) -> &'static [ResolverComputedMethod];
118    fn evaluate_computed(
119        &self,
120        method: &str,
121        args: &[Value],
122    ) -> std::result::Result<Value, Box<dyn std::error::Error>>;
123    fn typescript_interface(&self) -> Option<&'static str> {
124        None
125    }
126    fn typescript_schema(&self) -> Option<ResolverTypeScriptSchema> {
127        None
128    }
129}
130
131pub struct ResolverRegistry {
132    resolvers: HashMap<String, Box<dyn ResolverDefinition>>,
133}
134
135impl Default for ResolverRegistry {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl ResolverRegistry {
142    pub fn new() -> Self {
143        Self {
144            resolvers: HashMap::new(),
145        }
146    }
147
148    pub fn register(&mut self, resolver: Box<dyn ResolverDefinition>) {
149        self.resolvers.insert(resolver.name().to_string(), resolver);
150    }
151
152    pub fn resolver(&self, name: &str) -> Option<&dyn ResolverDefinition> {
153        self.resolvers.get(name).map(|resolver| resolver.as_ref())
154    }
155
156    pub fn definitions(&self) -> impl Iterator<Item = &dyn ResolverDefinition> {
157        self.resolvers.values().map(|resolver| resolver.as_ref())
158    }
159
160    pub fn is_output_type(&self, type_name: &str) -> bool {
161        self.resolvers
162            .values()
163            .any(|resolver| resolver.output_type() == type_name)
164    }
165
166    pub fn evaluate_computed(
167        &self,
168        resolver: &str,
169        method: &str,
170        args: &[Value],
171    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
172        let resolver_impl = self
173            .resolver(resolver)
174            .ok_or_else(|| format!("Unknown resolver '{}'", resolver))?;
175
176        let method_spec = resolver_impl
177            .computed_methods()
178            .iter()
179            .find(|spec| spec.name == method)
180            .ok_or_else(|| {
181                format!(
182                    "Resolver '{}' does not provide method '{}'",
183                    resolver, method
184                )
185            })?;
186
187        if method_spec.arg_count != args.len() {
188            return Err(format!(
189                "Resolver '{}' method '{}' expects {} args, got {}",
190                resolver,
191                method,
192                method_spec.arg_count,
193                args.len()
194            )
195            .into());
196        }
197
198        resolver_impl.evaluate_computed(method, args)
199    }
200
201    pub fn validate_computed_expr(
202        &self,
203        expr: &crate::ast::ComputedExpr,
204        errors: &mut Vec<String>,
205    ) {
206        match expr {
207            crate::ast::ComputedExpr::ResolverComputed {
208                resolver,
209                method,
210                args,
211            } => {
212                let resolver_impl = self.resolver(resolver);
213                if resolver_impl.is_none() {
214                    errors.push(format!("Unknown resolver '{}'", resolver));
215                } else if let Some(resolver_impl) = resolver_impl {
216                    let method_spec = resolver_impl
217                        .computed_methods()
218                        .iter()
219                        .find(|spec| spec.name == method);
220                    if let Some(method_spec) = method_spec {
221                        if method_spec.arg_count != args.len() {
222                            errors.push(format!(
223                                "Resolver '{}' method '{}' expects {} args, got {}",
224                                resolver,
225                                method,
226                                method_spec.arg_count,
227                                args.len()
228                            ));
229                        }
230                    } else {
231                        errors.push(format!(
232                            "Resolver '{}' does not provide method '{}'",
233                            resolver, method
234                        ));
235                    }
236                }
237
238                for arg in args {
239                    self.validate_computed_expr(arg, errors);
240                }
241            }
242            crate::ast::ComputedExpr::FieldRef { .. }
243            | crate::ast::ComputedExpr::Literal { .. }
244            | crate::ast::ComputedExpr::None
245            | crate::ast::ComputedExpr::Var { .. }
246            | crate::ast::ComputedExpr::ByteArray { .. }
247            | crate::ast::ComputedExpr::ContextSlot
248            | crate::ast::ComputedExpr::ContextTimestamp => {}
249            crate::ast::ComputedExpr::UnwrapOr { expr, .. }
250            | crate::ast::ComputedExpr::Cast { expr, .. }
251            | crate::ast::ComputedExpr::Paren { expr }
252            | crate::ast::ComputedExpr::Some { value: expr }
253            | crate::ast::ComputedExpr::Slice { expr, .. }
254            | crate::ast::ComputedExpr::Index { expr, .. }
255            | crate::ast::ComputedExpr::U64FromLeBytes { bytes: expr }
256            | crate::ast::ComputedExpr::U64FromBeBytes { bytes: expr }
257            | crate::ast::ComputedExpr::JsonToBytes { expr }
258            | crate::ast::ComputedExpr::Unary { expr, .. } => {
259                self.validate_computed_expr(expr, errors);
260            }
261            crate::ast::ComputedExpr::Binary { left, right, .. } => {
262                self.validate_computed_expr(left, errors);
263                self.validate_computed_expr(right, errors);
264            }
265            crate::ast::ComputedExpr::MethodCall { expr, args, .. } => {
266                self.validate_computed_expr(expr, errors);
267                for arg in args {
268                    self.validate_computed_expr(arg, errors);
269                }
270            }
271            crate::ast::ComputedExpr::Let { value, body, .. } => {
272                self.validate_computed_expr(value, errors);
273                self.validate_computed_expr(body, errors);
274            }
275            crate::ast::ComputedExpr::If {
276                condition,
277                then_branch,
278                else_branch,
279            } => {
280                self.validate_computed_expr(condition, errors);
281                self.validate_computed_expr(then_branch, errors);
282                self.validate_computed_expr(else_branch, errors);
283            }
284            crate::ast::ComputedExpr::Closure { body, .. } => {
285                self.validate_computed_expr(body, errors);
286            }
287        }
288    }
289}
290
291static BUILTIN_RESOLVER_REGISTRY: OnceLock<ResolverRegistry> = OnceLock::new();
292
293pub fn register_builtin_resolvers(registry: &mut ResolverRegistry) {
294    registry.register(Box::new(TokenMetadataResolver));
295}
296
297pub fn builtin_resolver_registry() -> &'static ResolverRegistry {
298    BUILTIN_RESOLVER_REGISTRY.get_or_init(|| {
299        let mut registry = ResolverRegistry::new();
300        register_builtin_resolvers(&mut registry);
301        registry
302    })
303}
304
305pub fn evaluate_resolver_computed(
306    resolver: &str,
307    method: &str,
308    args: &[Value],
309) -> std::result::Result<Value, Box<dyn std::error::Error>> {
310    builtin_resolver_registry().evaluate_computed(resolver, method, args)
311}
312
313pub fn validate_resolver_computed_specs(
314    specs: &[crate::ast::ComputedFieldSpec],
315) -> std::result::Result<(), Box<dyn std::error::Error>> {
316    let registry = builtin_resolver_registry();
317    let mut errors = Vec::new();
318
319    for spec in specs {
320        registry.validate_computed_expr(&spec.expression, &mut errors);
321    }
322
323    if errors.is_empty() {
324        Ok(())
325    } else {
326        Err(errors.join("\n").into())
327    }
328}
329
330pub fn is_resolver_output_type(type_name: &str) -> bool {
331    builtin_resolver_registry().is_output_type(type_name)
332}
333
334const DEFAULT_DAS_BATCH_SIZE: usize = 100;
335const DEFAULT_DAS_TIMEOUT_SECS: u64 = 10;
336const DAS_API_ENDPOINT_ENV: &str = "DAS_API_ENDPOINT";
337const DAS_API_BATCH_ENV: &str = "DAS_API_BATCH_SIZE";
338
339pub struct TokenMetadataResolverClient {
340    endpoint: String,
341    client: reqwest::Client,
342    batch_size: usize,
343}
344
345impl TokenMetadataResolverClient {
346    pub fn new(
347        endpoint: String,
348        batch_size: usize,
349    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
350        let client = reqwest::Client::builder()
351            .timeout(std::time::Duration::from_secs(DEFAULT_DAS_TIMEOUT_SECS))
352            .build()?;
353
354        Ok(Self {
355            endpoint,
356            client,
357            batch_size: batch_size.max(1),
358        })
359    }
360
361    pub fn from_env(
362    ) -> Result<Option<Self>, Box<dyn std::error::Error + Send + Sync>> {
363        let Some(endpoint) = std::env::var(DAS_API_ENDPOINT_ENV).ok() else {
364            return Ok(None);
365        };
366
367        let batch_size = std::env::var(DAS_API_BATCH_ENV)
368            .ok()
369            .and_then(|value| value.parse::<usize>().ok())
370            .unwrap_or(DEFAULT_DAS_BATCH_SIZE);
371
372        Ok(Some(Self::new(endpoint, batch_size)?))
373    }
374
375    pub async fn resolve_token_metadata(
376        &self,
377        mints: &[String],
378    ) -> Result<HashMap<String, Value>, Box<dyn std::error::Error + Send + Sync>> {
379        let mut unique = HashSet::new();
380        let mut deduped = Vec::new();
381
382        for mint in mints {
383            if mint.is_empty() {
384                continue;
385            }
386            if unique.insert(mint.clone()) {
387                deduped.push(mint.clone());
388            }
389        }
390
391        let mut results = HashMap::new();
392        if deduped.is_empty() {
393            return Ok(results);
394        }
395
396        for chunk in deduped.chunks(self.batch_size) {
397            let assets = self.fetch_assets(chunk).await?;
398            for asset in assets {
399                if let Some((mint, value)) = Self::build_token_metadata(&asset) {
400                    results.insert(mint, value);
401                }
402            }
403        }
404
405        Ok(results)
406    }
407
408    async fn fetch_assets(
409        &self,
410        ids: &[String],
411    ) -> Result<Vec<Value>, Box<dyn std::error::Error + Send + Sync>> {
412        let payload = serde_json::json!({
413            "jsonrpc": "2.0",
414            "id": "1",
415            "method": "getAssetBatch",
416            "params": {
417                "ids": ids,
418                "options": {
419                    "showFungible": true,
420                },
421            },
422        });
423
424        let response = self.client.post(&self.endpoint).json(&payload).send().await?;
425        let response = response.error_for_status()?;
426        let value = response.json::<Value>().await?;
427
428        if let Some(error) = value.get("error") {
429            return Err(format!("Resolver response error: {}", error).into());
430        }
431
432        let assets = value
433            .get("result")
434            .and_then(|result| match result {
435                Value::Array(items) => Some(items.clone()),
436                Value::Object(obj) => obj
437                    .get("items")
438                    .and_then(|items| items.as_array())
439                    .cloned(),
440                _ => None,
441            })
442            .ok_or_else(|| "Resolver response missing result".to_string())?;
443
444        let assets = assets.into_iter().filter(|a| !a.is_null()).collect();
445        Ok(assets)
446    }
447
448    fn build_token_metadata(asset: &Value) -> Option<(String, Value)> {
449        let mint = asset.get("id").and_then(|value| value.as_str())?.to_string();
450
451        let name = asset
452            .pointer("/content/metadata/name")
453            .and_then(|value| value.as_str());
454
455        let symbol = asset
456            .pointer("/content/metadata/symbol")
457            .and_then(|value| value.as_str());
458
459        let token_info = asset.get("token_info").or_else(|| asset.pointer("/content/token_info"));
460
461        let decimals = token_info
462            .and_then(|info| info.get("decimals"))
463            .and_then(|value| value.as_u64());
464
465        let logo_uri = asset
466            .pointer("/content/links/image")
467            .and_then(|value| value.as_str())
468            .or_else(|| asset.pointer("/content/links/image_uri").and_then(|value| value.as_str()));
469
470        let mut obj = serde_json::Map::new();
471        obj.insert("mint".to_string(), serde_json::json!(mint));
472        obj.insert(
473            "name".to_string(),
474            name.map(|value| serde_json::json!(value))
475                .unwrap_or(Value::Null),
476        );
477        obj.insert(
478            "symbol".to_string(),
479            symbol.map(|value| serde_json::json!(value))
480                .unwrap_or(Value::Null),
481        );
482        obj.insert(
483            "decimals".to_string(),
484            decimals
485                .map(|value| serde_json::json!(value))
486                .unwrap_or(Value::Null),
487        );
488        obj.insert(
489            "logo_uri".to_string(),
490            logo_uri
491                .map(|value| serde_json::json!(value))
492                .unwrap_or(Value::Null),
493        );
494
495        Some((mint, Value::Object(obj)))
496    }
497}
498
499// ============================================================================
500// URL Resolver Client - Fetch and parse data from external URLs
501// ============================================================================
502
503const DEFAULT_URL_TIMEOUT_SECS: u64 = 30;
504
505pub struct UrlResolverClient {
506    client: reqwest::Client,
507}
508
509impl Default for UrlResolverClient {
510    fn default() -> Self {
511        Self::new()
512    }
513}
514
515impl UrlResolverClient {
516    pub fn new() -> Self {
517        let client = reqwest::Client::builder()
518            .timeout(std::time::Duration::from_secs(DEFAULT_URL_TIMEOUT_SECS))
519            .build()
520            .expect("Failed to create HTTP client for URL resolver");
521
522        Self { client }
523    }
524
525    pub fn with_timeout(timeout_secs: u64) -> Self {
526        let client = reqwest::Client::builder()
527            .timeout(std::time::Duration::from_secs(timeout_secs))
528            .build()
529            .expect("Failed to create HTTP client for URL resolver");
530
531        Self { client }
532    }
533
534    /// Resolve a URL and return the parsed JSON response
535    pub async fn resolve(
536        &self,
537        url: &str,
538        method: &crate::ast::HttpMethod,
539    ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
540        if url.is_empty() {
541            return Err("URL is empty".into());
542        }
543
544        let response = match method {
545            crate::ast::HttpMethod::Get => self.client.get(url).send().await?,
546            crate::ast::HttpMethod::Post => self.client.post(url).send().await?,
547        };
548
549        let response = response.error_for_status()?;
550        let value = response.json::<Value>().await?;
551
552        Ok(value)
553    }
554
555    /// Resolve a URL and extract a specific JSON path from the response
556    pub async fn resolve_with_extract(
557        &self,
558        url: &str,
559        method: &crate::ast::HttpMethod,
560        extract_path: Option<&str>,
561    ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
562        let response = self.resolve(url, method).await?;
563
564        if let Some(path) = extract_path {
565            Self::extract_json_path(&response, path)
566        } else {
567            Ok(response)
568        }
569    }
570
571    /// Extract a value from a JSON object using dot-notation path
572    /// e.g., "data.image" extracts response["data"]["image"]
573    pub fn extract_json_path(
574        value: &Value,
575        path: &str,
576    ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
577        if path.is_empty() {
578            return Ok(value.clone());
579        }
580
581        let mut current = value;
582        for segment in path.split('.') {
583            // Try as object key first
584            if let Some(next) = current.get(segment) {
585                current = next;
586            } else if let Ok(index) = segment.parse::<usize>() {
587                // Try as array index
588                if let Some(next) = current.get(index) {
589                    current = next;
590                } else {
591                    return Err(format!("Index '{}' out of bounds in path '{}'", index, path).into());
592                }
593            } else {
594                return Err(format!("Key '{}' not found in path '{}'", segment, path).into());
595            }
596        }
597
598        Ok(current.clone())
599    }
600
601    /// Batch resolve multiple URLs in parallel with deduplication.
602    /// Returns raw JSON keyed by URL. Identical URLs are only fetched once.
603    pub async fn resolve_batch(
604        &self,
605        urls: &[(String, crate::ast::HttpMethod)],
606    ) -> HashMap<String, Value> {
607        let mut unique: HashMap<String, crate::ast::HttpMethod> = HashMap::new();
608        for (url, method) in urls {
609            if !url.is_empty() {
610                unique.entry(url.clone()).or_insert_with(|| method.clone());
611            }
612        }
613
614        let futures = unique
615            .into_iter()
616            .map(|(url, method)| async move {
617                let result = self.resolve(&url, &method).await;
618                (url, result)
619            });
620
621        join_all(futures)
622            .await
623            .into_iter()
624            .filter_map(|(url, result)| match result {
625                Ok(value) => Some((url, value)),
626                Err(e) => {
627                    tracing::warn!(url = %url, error = %e, "Failed to resolve URL");
628                    None
629                }
630            })
631            .collect()
632    }
633}
634
635/// Intermediate type for validated URL resolver requests,
636/// avoiding redundant pattern matching after partition.
637struct ValidUrlRequest {
638    url: String,
639    method: crate::ast::HttpMethod,
640    request: crate::vm::ResolverRequest,
641}
642
643/// Resolve a batch of URL resolver requests in parallel with deduplication,
644/// apply results to VM state, and requeue failures.
645///
646/// This is the single entry point for URL resolution at runtime —
647/// it owns the full lifecycle: validate, fetch, apply, requeue.
648pub async fn resolve_url_batch(
649    vm: &std::sync::Mutex<crate::vm::VmContext>,
650    bytecode: &crate::compiler::MultiEntityBytecode,
651    url_client: &UrlResolverClient,
652    requests: Vec<crate::vm::ResolverRequest>,
653) -> Vec<crate::Mutation> {
654    if requests.is_empty() {
655        return Vec::new();
656    }
657
658    // Partition into valid and invalid in a single pass
659    let mut valid = Vec::with_capacity(requests.len());
660    let mut invalid = Vec::new();
661
662    for request in requests {
663        if let crate::ast::ResolverType::Url(ref config) = request.resolver {
664            match &request.input {
665                serde_json::Value::String(s) if !s.is_empty() => {
666                    valid.push(ValidUrlRequest {
667                        url: s.clone(),
668                        method: config.method.clone(),
669                        request,
670                    });
671                }
672                _ => {
673                    tracing::warn!(
674                        "URL resolver input is not a non-empty string: {:?}",
675                        request.input
676                    );
677                    invalid.push(request);
678                }
679            }
680        }
681    }
682
683    if !invalid.is_empty() {
684        let mut vm = vm.lock().unwrap_or_else(|e| e.into_inner());
685        vm.restore_resolver_requests(invalid);
686    }
687
688    if valid.is_empty() {
689        return Vec::new();
690    }
691
692    // Build deduplicated batch input
693    let batch_input: Vec<_> = valid
694        .iter()
695        .map(|v| (v.url.clone(), v.method.clone()))
696        .collect();
697
698    let results = url_client.resolve_batch(&batch_input).await;
699
700    // Apply results to VM state, requeue anything that didn't resolve
701    let mut vm = vm.lock().unwrap_or_else(|e| e.into_inner());
702    let mut mutations = Vec::new();
703    let mut failed = Vec::new();
704
705    for entry in valid {
706        match results.get(&entry.url) {
707            Some(resolved_value) => {
708                match vm.apply_resolver_result(bytecode, &entry.request.cache_key, resolved_value.clone()) {
709                    Ok(mut new_mutations) => mutations.append(&mut new_mutations),
710                    Err(err) => {
711                        tracing::warn!(url = %entry.url, "Failed to apply URL resolver result: {}", err);
712                    }
713                }
714            }
715            None => {
716                tracing::warn!(url = %entry.url, "URL resolver request failed, re-queuing");
717                failed.push(entry.request);
718            }
719        }
720    }
721
722    if !failed.is_empty() {
723        vm.restore_resolver_requests(failed);
724    }
725
726    mutations
727}
728
729struct TokenMetadataResolver;
730
731const TOKEN_METADATA_METHODS: &[ResolverComputedMethod] = &[
732    ResolverComputedMethod {
733        name: "ui_amount",
734        arg_count: 2,
735    },
736    ResolverComputedMethod {
737        name: "raw_amount",
738        arg_count: 2,
739    },
740];
741
742impl TokenMetadataResolver {
743    fn optional_f64(value: &Value) -> Option<f64> {
744        if value.is_null() {
745            return None;
746        }
747        match value {
748            Value::Number(number) => number.as_f64(),
749            Value::String(text) => text.parse::<f64>().ok(),
750            _ => None,
751        }
752    }
753
754    fn optional_u8(value: &Value) -> Option<u8> {
755        if value.is_null() {
756            return None;
757        }
758        match value {
759            Value::Number(number) => number
760                .as_u64()
761                .or_else(|| {
762                    number
763                        .as_i64()
764                        .and_then(|v| if v >= 0 { Some(v as u64) } else { None })
765                })
766                .and_then(|v| u8::try_from(v).ok()),
767            Value::String(text) => text.parse::<u8>().ok(),
768            _ => None,
769        }
770    }
771
772    fn evaluate_ui_amount(
773        args: &[Value],
774    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
775        let raw_value = Self::optional_f64(&args[0]);
776        let decimals = Self::optional_u8(&args[1]);
777
778        match (raw_value, decimals) {
779            (Some(value), Some(decimals)) => {
780                let factor = 10_f64.powi(decimals as i32);
781                let result = value / factor;
782                if result.is_finite() {
783                    serde_json::Number::from_f64(result)
784                        .map(Value::Number)
785                        .ok_or_else(|| "Failed to serialize ui_amount".into())
786                } else {
787                    Err("ui_amount result is not finite".into())
788                }
789            }
790            _ => Ok(Value::Null),
791        }
792    }
793
794    fn evaluate_raw_amount(
795        args: &[Value],
796    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
797        let ui_value = Self::optional_f64(&args[0]);
798        let decimals = Self::optional_u8(&args[1]);
799
800        match (ui_value, decimals) {
801            (Some(value), Some(decimals)) => {
802                let factor = 10_f64.powi(decimals as i32);
803                let result = value * factor;
804                if !result.is_finite() || result < 0.0 {
805                    return Err("raw_amount result is not finite".into());
806                }
807                let rounded = result.round();
808                if rounded > u64::MAX as f64 {
809                    return Err("raw_amount result exceeds u64".into());
810                }
811                Ok(Value::Number(serde_json::Number::from(rounded as u64)))
812            }
813            _ => Ok(Value::Null),
814        }
815    }
816}
817
818impl ResolverDefinition for TokenMetadataResolver {
819    fn name(&self) -> &'static str {
820        "TokenMetadata"
821    }
822
823    fn output_type(&self) -> &'static str {
824        "TokenMetadata"
825    }
826
827    fn computed_methods(&self) -> &'static [ResolverComputedMethod] {
828        TOKEN_METADATA_METHODS
829    }
830
831    fn evaluate_computed(
832        &self,
833        method: &str,
834        args: &[Value],
835    ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
836        match method {
837            "ui_amount" => Self::evaluate_ui_amount(args),
838            "raw_amount" => Self::evaluate_raw_amount(args),
839            _ => Err(format!("Unknown TokenMetadata method '{}'", method).into()),
840        }
841    }
842
843    fn typescript_interface(&self) -> Option<&'static str> {
844        Some(
845            r#"export interface TokenMetadata {
846  mint: string;
847  name?: string | null;
848  symbol?: string | null;
849  decimals?: number | null;
850  logo_uri?: string | null;
851}"#,
852        )
853    }
854
855    fn typescript_schema(&self) -> Option<ResolverTypeScriptSchema> {
856        Some(ResolverTypeScriptSchema {
857            name: "TokenMetadataSchema",
858            definition: r#"export const TokenMetadataSchema = z.object({
859  mint: z.string(),
860  name: z.string().nullable().optional(),
861  symbol: z.string().nullable().optional(),
862  decimals: z.number().nullable().optional(),
863  logo_uri: z.string().nullable().optional(),
864});"#,
865        })
866    }
867}
868
869impl<'a> InstructionContext<'a> {
870    pub fn new(
871        accounts: HashMap<String, String>,
872        state_id: u32,
873        reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
874    ) -> Self {
875        Self {
876            accounts,
877            state_id,
878            reverse_lookup_tx,
879            pending_updates: Vec::new(),
880            registers: None,
881            state_reg: None,
882            compiled_paths: None,
883            instruction_data: None,
884            slot: None,
885            signature: None,
886            timestamp: None,
887            dirty_tracker: crate::vm::DirtyTracker::new(),
888        }
889    }
890
891    #[allow(clippy::too_many_arguments)]
892    pub fn with_metrics(
893        accounts: HashMap<String, String>,
894        state_id: u32,
895        reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
896        registers: &'a mut Vec<crate::vm::RegisterValue>,
897        state_reg: crate::vm::Register,
898        compiled_paths: &'a HashMap<String, crate::metrics_context::CompiledPath>,
899        instruction_data: &'a serde_json::Value,
900        slot: Option<u64>,
901        signature: Option<String>,
902        timestamp: i64,
903    ) -> Self {
904        Self {
905            accounts,
906            state_id,
907            reverse_lookup_tx,
908            pending_updates: Vec::new(),
909            registers: Some(registers),
910            state_reg: Some(state_reg),
911            compiled_paths: Some(compiled_paths),
912            instruction_data: Some(instruction_data),
913            slot,
914            signature,
915            timestamp: Some(timestamp),
916            dirty_tracker: crate::vm::DirtyTracker::new(),
917        }
918    }
919
920    /// Get an account address by its name from the instruction
921    pub fn account(&self, name: &str) -> Option<String> {
922        self.accounts.get(name).cloned()
923    }
924
925    /// Register a reverse lookup: PDA address -> seed value
926    /// This also flushes any pending account updates for this PDA
927    ///
928    /// The pending account updates are accumulated internally and can be retrieved
929    /// via `take_pending_updates()` after all hooks have executed.
930    pub fn register_pda_reverse_lookup(&mut self, pda_address: &str, seed_value: &str) {
931        let pending = self
932            .reverse_lookup_tx
933            .update(pda_address.to_string(), seed_value.to_string());
934        self.pending_updates.extend(pending);
935    }
936
937    /// Take all accumulated pending updates
938    ///
939    /// This should be called after all instruction hooks have executed to retrieve
940    /// any pending account updates that need to be reprocessed.
941    pub fn take_pending_updates(&mut self) -> Vec<crate::vm::PendingAccountUpdate> {
942        std::mem::take(&mut self.pending_updates)
943    }
944
945    pub fn dirty_tracker(&self) -> &crate::vm::DirtyTracker {
946        &self.dirty_tracker
947    }
948
949    pub fn dirty_tracker_mut(&mut self) -> &mut crate::vm::DirtyTracker {
950        &mut self.dirty_tracker
951    }
952
953    /// Get the current state register value (for generating mutations)
954    pub fn state_value(&self) -> Option<&serde_json::Value> {
955        if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
956            Some(&registers[state_reg])
957        } else {
958            None
959        }
960    }
961
962    /// Get a field value from the entity state
963    /// This allows reading aggregated values or other entity fields
964    pub fn get<T: serde::de::DeserializeOwned>(&self, field_path: &str) -> Option<T> {
965        if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
966            let state = &registers[state_reg];
967            self.get_nested_value(state, field_path)
968                .and_then(|v| serde_json::from_value(v.clone()).ok())
969        } else {
970            None
971        }
972    }
973
974    pub fn set<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
975        if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
976            let serialized = serde_json::to_value(value).ok();
977            if let Some(val) = serialized {
978                Self::set_nested_value_static(&mut registers[state_reg], field_path, val);
979                self.dirty_tracker.mark_replaced(field_path);
980                println!("      ✓ Set field '{}' and marked as dirty", field_path);
981            }
982        } else {
983            println!("      ⚠️  Cannot set field '{}': metrics not configured (registers={}, state_reg={:?})", 
984                field_path, self.registers.is_some(), self.state_reg);
985        }
986    }
987
988    pub fn increment(&mut self, field_path: &str, amount: i64) {
989        let current = self.get::<i64>(field_path).unwrap_or(0);
990        self.set(field_path, current + amount);
991    }
992
993    pub fn append<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
994        if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
995            let serialized = serde_json::to_value(&value).ok();
996            if let Some(val) = serialized {
997                Self::append_to_array_static(&mut registers[state_reg], field_path, val.clone());
998                self.dirty_tracker.mark_appended(field_path, val);
999                println!(
1000                    "      ✓ Appended to '{}' and marked as appended",
1001                    field_path
1002                );
1003            }
1004        } else {
1005            println!(
1006                "      ⚠️  Cannot append to '{}': metrics not configured",
1007                field_path
1008            );
1009        }
1010    }
1011
1012    fn append_to_array_static(
1013        value: &mut serde_json::Value,
1014        path: &str,
1015        new_value: serde_json::Value,
1016    ) {
1017        let segments: Vec<&str> = path.split('.').collect();
1018        if segments.is_empty() {
1019            return;
1020        }
1021
1022        let mut current = value;
1023        for segment in &segments[..segments.len() - 1] {
1024            if !current.is_object() {
1025                *current = serde_json::json!({});
1026            }
1027            let obj = current.as_object_mut().unwrap();
1028            current = obj
1029                .entry(segment.to_string())
1030                .or_insert(serde_json::json!({}));
1031        }
1032
1033        let last_segment = segments[segments.len() - 1];
1034        if !current.is_object() {
1035            *current = serde_json::json!({});
1036        }
1037        let obj = current.as_object_mut().unwrap();
1038        let arr = obj
1039            .entry(last_segment.to_string())
1040            .or_insert_with(|| serde_json::json!([]));
1041        if let Some(arr) = arr.as_array_mut() {
1042            arr.push(new_value);
1043        }
1044    }
1045
1046    fn get_nested_value<'b>(
1047        &self,
1048        value: &'b serde_json::Value,
1049        path: &str,
1050    ) -> Option<&'b serde_json::Value> {
1051        let mut current = value;
1052        for segment in path.split('.') {
1053            current = current.get(segment)?;
1054        }
1055        Some(current)
1056    }
1057
1058    fn set_nested_value_static(
1059        value: &mut serde_json::Value,
1060        path: &str,
1061        new_value: serde_json::Value,
1062    ) {
1063        let segments: Vec<&str> = path.split('.').collect();
1064        if segments.is_empty() {
1065            return;
1066        }
1067
1068        let mut current = value;
1069        for segment in &segments[..segments.len() - 1] {
1070            if !current.is_object() {
1071                *current = serde_json::json!({});
1072            }
1073            let obj = current.as_object_mut().unwrap();
1074            current = obj
1075                .entry(segment.to_string())
1076                .or_insert(serde_json::json!({}));
1077        }
1078
1079        if !current.is_object() {
1080            *current = serde_json::json!({});
1081        }
1082        if let Some(obj) = current.as_object_mut() {
1083            obj.insert(segments[segments.len() - 1].to_string(), new_value);
1084        }
1085    }
1086
1087    /// Access instruction data field
1088    pub fn data<T: serde::de::DeserializeOwned>(&self, field: &str) -> Option<T> {
1089        self.instruction_data
1090            .and_then(|data| data.get(field))
1091            .and_then(|v| serde_json::from_value(v.clone()).ok())
1092    }
1093
1094    /// Get the current timestamp
1095    pub fn timestamp(&self) -> i64 {
1096        self.timestamp.unwrap_or(0)
1097    }
1098
1099    /// Get the current slot
1100    pub fn slot(&self) -> Option<u64> {
1101        self.slot
1102    }
1103
1104    /// Get the current signature
1105    pub fn signature(&self) -> Option<&str> {
1106        self.signature.as_deref()
1107    }
1108}