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