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
10pub 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 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 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
54pub enum KeyResolution {
56 Found(String),
58
59 QueueUntil(&'static [u8]),
62
63 Skip,
65}
66
67pub 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
523const 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 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 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 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 if let Some(next) = current.get(segment) {
609 current = next;
610 } else if let Ok(index) = segment.parse::<usize>() {
611 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 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
659struct 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 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 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 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 use sha3::{Digest, Keccak256};
712 let hash = Keccak256::digest(&input);
713
714 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 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 let slot_hash = crate::slot_hash_cache::get_slot_hash(slot);
756
757 match slot_hash {
758 Some(hash) => {
759 match bs58::decode(&hash).into_vec() {
762 Ok(bytes) if bytes.len() == 32 => {
763 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 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 pub fn account(&self, name: &str) -> Option<String> {
1065 self.accounts.get(name).cloned()
1066 }
1067
1068 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 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 pub fn state_value(&self) -> Option<&serde_json::Value> {
1098 let state_reg = self.state_reg?;
1099 let registers = self.registers()?;
1100 Some(®isters[state_reg])
1101 }
1102
1103 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 = ®isters[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 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 pub fn timestamp(&self) -> i64 {
1249 self.timestamp.unwrap_or(0)
1250 }
1251
1252 pub fn slot(&self) -> Option<u64> {
1254 self.slot
1255 }
1256
1257 pub fn signature(&self) -> Option<&str> {
1259 self.signature.as_deref()
1260 }
1261}