Skip to main content

arete_interpreter/
resolvers.rs

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