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
9pub 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 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 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
53pub enum KeyResolution {
55 Found(String),
57
58 QueueUntil(&'static [u8]),
61
62 Skip,
64}
65
66pub 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
505const 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 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 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 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 if let Some(next) = current.get(segment) {
591 current = next;
592 } else if let Ok(index) = segment.parse::<usize>() {
593 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 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
641struct ValidUrlRequest {
644 url: String,
645 method: crate::ast::HttpMethod,
646 request: crate::vm::ResolverRequest,
647}
648
649pub 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 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 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 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
737struct 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 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 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 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 use sha3::{Digest, Keccak256};
787 let hash = Keccak256::digest(&input);
788
789 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 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 let slot_hash = crate::slot_hash_cache::get_slot_hash(slot);
834
835 match slot_hash {
836 Some(hash) => {
837 match bs58::decode(&hash).into_vec() {
840 Ok(bytes) if bytes.len() == 32 => {
841 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 pub fn account(&self, name: &str) -> Option<String> {
1106 self.accounts.get(name).cloned()
1107 }
1108
1109 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 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 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(®isters[state_reg])
1141 } else {
1142 None
1143 }
1144 }
1145
1146 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 = ®isters[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 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 pub fn timestamp(&self) -> i64 {
1280 self.timestamp.unwrap_or(0)
1281 }
1282
1283 pub fn slot(&self) -> Option<u64> {
1285 self.slot
1286 }
1287
1288 pub fn signature(&self) -> Option<&str> {
1290 self.signature.as_deref()
1291 }
1292}