Skip to main content

hyperstack_interpreter/
resolvers.rs

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