1use std::collections::{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}
130
131pub struct ResolverRegistry {
132 resolvers: HashMap<String, Box<dyn ResolverDefinition>>,
133}
134
135impl Default for ResolverRegistry {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl ResolverRegistry {
142 pub fn new() -> Self {
143 Self {
144 resolvers: HashMap::new(),
145 }
146 }
147
148 pub fn register(&mut self, resolver: Box<dyn ResolverDefinition>) {
149 self.resolvers.insert(resolver.name().to_string(), resolver);
150 }
151
152 pub fn resolver(&self, name: &str) -> Option<&dyn ResolverDefinition> {
153 self.resolvers.get(name).map(|resolver| resolver.as_ref())
154 }
155
156 pub fn definitions(&self) -> impl Iterator<Item = &dyn ResolverDefinition> {
157 self.resolvers.values().map(|resolver| resolver.as_ref())
158 }
159
160 pub fn is_output_type(&self, type_name: &str) -> bool {
161 self.resolvers
162 .values()
163 .any(|resolver| resolver.output_type() == type_name)
164 }
165
166 pub fn evaluate_computed(
167 &self,
168 resolver: &str,
169 method: &str,
170 args: &[Value],
171 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
172 let resolver_impl = self
173 .resolver(resolver)
174 .ok_or_else(|| format!("Unknown resolver '{}'", resolver))?;
175
176 let method_spec = resolver_impl
177 .computed_methods()
178 .iter()
179 .find(|spec| spec.name == method)
180 .ok_or_else(|| {
181 format!(
182 "Resolver '{}' does not provide method '{}'",
183 resolver, method
184 )
185 })?;
186
187 if method_spec.arg_count != args.len() {
188 return Err(format!(
189 "Resolver '{}' method '{}' expects {} args, got {}",
190 resolver,
191 method,
192 method_spec.arg_count,
193 args.len()
194 )
195 .into());
196 }
197
198 resolver_impl.evaluate_computed(method, args)
199 }
200
201 pub fn validate_computed_expr(
202 &self,
203 expr: &crate::ast::ComputedExpr,
204 errors: &mut Vec<String>,
205 ) {
206 match expr {
207 crate::ast::ComputedExpr::ResolverComputed {
208 resolver,
209 method,
210 args,
211 } => {
212 let resolver_impl = self.resolver(resolver);
213 if resolver_impl.is_none() {
214 errors.push(format!("Unknown resolver '{}'", resolver));
215 } else if let Some(resolver_impl) = resolver_impl {
216 let method_spec = resolver_impl
217 .computed_methods()
218 .iter()
219 .find(|spec| spec.name == method);
220 if let Some(method_spec) = method_spec {
221 if method_spec.arg_count != args.len() {
222 errors.push(format!(
223 "Resolver '{}' method '{}' expects {} args, got {}",
224 resolver,
225 method,
226 method_spec.arg_count,
227 args.len()
228 ));
229 }
230 } else {
231 errors.push(format!(
232 "Resolver '{}' does not provide method '{}'",
233 resolver, method
234 ));
235 }
236 }
237
238 for arg in args {
239 self.validate_computed_expr(arg, errors);
240 }
241 }
242 crate::ast::ComputedExpr::FieldRef { .. }
243 | crate::ast::ComputedExpr::Literal { .. }
244 | crate::ast::ComputedExpr::None
245 | crate::ast::ComputedExpr::Var { .. }
246 | crate::ast::ComputedExpr::ByteArray { .. }
247 | crate::ast::ComputedExpr::ContextSlot
248 | crate::ast::ComputedExpr::ContextTimestamp => {}
249 crate::ast::ComputedExpr::UnwrapOr { expr, .. }
250 | crate::ast::ComputedExpr::Cast { expr, .. }
251 | crate::ast::ComputedExpr::Paren { expr }
252 | crate::ast::ComputedExpr::Some { value: expr }
253 | crate::ast::ComputedExpr::Slice { expr, .. }
254 | crate::ast::ComputedExpr::Index { expr, .. }
255 | crate::ast::ComputedExpr::U64FromLeBytes { bytes: expr }
256 | crate::ast::ComputedExpr::U64FromBeBytes { bytes: expr }
257 | crate::ast::ComputedExpr::JsonToBytes { expr }
258 | crate::ast::ComputedExpr::Unary { expr, .. } => {
259 self.validate_computed_expr(expr, errors);
260 }
261 crate::ast::ComputedExpr::Binary { left, right, .. } => {
262 self.validate_computed_expr(left, errors);
263 self.validate_computed_expr(right, errors);
264 }
265 crate::ast::ComputedExpr::MethodCall { expr, args, .. } => {
266 self.validate_computed_expr(expr, errors);
267 for arg in args {
268 self.validate_computed_expr(arg, errors);
269 }
270 }
271 crate::ast::ComputedExpr::Let { value, body, .. } => {
272 self.validate_computed_expr(value, errors);
273 self.validate_computed_expr(body, errors);
274 }
275 crate::ast::ComputedExpr::If {
276 condition,
277 then_branch,
278 else_branch,
279 } => {
280 self.validate_computed_expr(condition, errors);
281 self.validate_computed_expr(then_branch, errors);
282 self.validate_computed_expr(else_branch, errors);
283 }
284 crate::ast::ComputedExpr::Closure { body, .. } => {
285 self.validate_computed_expr(body, errors);
286 }
287 }
288 }
289}
290
291static BUILTIN_RESOLVER_REGISTRY: OnceLock<ResolverRegistry> = OnceLock::new();
292
293pub fn register_builtin_resolvers(registry: &mut ResolverRegistry) {
294 registry.register(Box::new(TokenMetadataResolver));
295}
296
297pub fn builtin_resolver_registry() -> &'static ResolverRegistry {
298 BUILTIN_RESOLVER_REGISTRY.get_or_init(|| {
299 let mut registry = ResolverRegistry::new();
300 register_builtin_resolvers(&mut registry);
301 registry
302 })
303}
304
305pub fn evaluate_resolver_computed(
306 resolver: &str,
307 method: &str,
308 args: &[Value],
309) -> std::result::Result<Value, Box<dyn std::error::Error>> {
310 builtin_resolver_registry().evaluate_computed(resolver, method, args)
311}
312
313pub fn validate_resolver_computed_specs(
314 specs: &[crate::ast::ComputedFieldSpec],
315) -> std::result::Result<(), Box<dyn std::error::Error>> {
316 let registry = builtin_resolver_registry();
317 let mut errors = Vec::new();
318
319 for spec in specs {
320 registry.validate_computed_expr(&spec.expression, &mut errors);
321 }
322
323 if errors.is_empty() {
324 Ok(())
325 } else {
326 Err(errors.join("\n").into())
327 }
328}
329
330pub fn is_resolver_output_type(type_name: &str) -> bool {
331 builtin_resolver_registry().is_output_type(type_name)
332}
333
334const DEFAULT_DAS_BATCH_SIZE: usize = 100;
335const DEFAULT_DAS_TIMEOUT_SECS: u64 = 10;
336const DAS_API_ENDPOINT_ENV: &str = "DAS_API_ENDPOINT";
337const DAS_API_BATCH_ENV: &str = "DAS_API_BATCH_SIZE";
338
339pub struct TokenMetadataResolverClient {
340 endpoint: String,
341 client: reqwest::Client,
342 batch_size: usize,
343}
344
345impl TokenMetadataResolverClient {
346 pub fn new(
347 endpoint: String,
348 batch_size: usize,
349 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
350 let client = reqwest::Client::builder()
351 .timeout(std::time::Duration::from_secs(DEFAULT_DAS_TIMEOUT_SECS))
352 .build()?;
353
354 Ok(Self {
355 endpoint,
356 client,
357 batch_size: batch_size.max(1),
358 })
359 }
360
361 pub fn from_env(
362 ) -> Result<Option<Self>, Box<dyn std::error::Error + Send + Sync>> {
363 let Some(endpoint) = std::env::var(DAS_API_ENDPOINT_ENV).ok() else {
364 return Ok(None);
365 };
366
367 let batch_size = std::env::var(DAS_API_BATCH_ENV)
368 .ok()
369 .and_then(|value| value.parse::<usize>().ok())
370 .unwrap_or(DEFAULT_DAS_BATCH_SIZE);
371
372 Ok(Some(Self::new(endpoint, batch_size)?))
373 }
374
375 pub async fn resolve_token_metadata(
376 &self,
377 mints: &[String],
378 ) -> Result<HashMap<String, Value>, Box<dyn std::error::Error + Send + Sync>> {
379 let mut unique = HashSet::new();
380 let mut deduped = Vec::new();
381
382 for mint in mints {
383 if mint.is_empty() {
384 continue;
385 }
386 if unique.insert(mint.clone()) {
387 deduped.push(mint.clone());
388 }
389 }
390
391 let mut results = HashMap::new();
392 if deduped.is_empty() {
393 return Ok(results);
394 }
395
396 for chunk in deduped.chunks(self.batch_size) {
397 let assets = self.fetch_assets(chunk).await?;
398 for asset in assets {
399 if let Some((mint, value)) = Self::build_token_metadata(&asset) {
400 results.insert(mint, value);
401 }
402 }
403 }
404
405 Ok(results)
406 }
407
408 async fn fetch_assets(
409 &self,
410 ids: &[String],
411 ) -> Result<Vec<Value>, Box<dyn std::error::Error + Send + Sync>> {
412 let payload = serde_json::json!({
413 "jsonrpc": "2.0",
414 "id": "1",
415 "method": "getAssetBatch",
416 "params": {
417 "ids": ids,
418 "options": {
419 "showFungible": true,
420 },
421 },
422 });
423
424 let response = self.client.post(&self.endpoint).json(&payload).send().await?;
425 let response = response.error_for_status()?;
426 let value = response.json::<Value>().await?;
427
428 if let Some(error) = value.get("error") {
429 return Err(format!("Resolver response error: {}", error).into());
430 }
431
432 let assets = value
433 .get("result")
434 .and_then(|result| match result {
435 Value::Array(items) => Some(items.clone()),
436 Value::Object(obj) => obj
437 .get("items")
438 .and_then(|items| items.as_array())
439 .cloned(),
440 _ => None,
441 })
442 .ok_or_else(|| "Resolver response missing result".to_string())?;
443
444 let assets = assets.into_iter().filter(|a| !a.is_null()).collect();
445 Ok(assets)
446 }
447
448 fn build_token_metadata(asset: &Value) -> Option<(String, Value)> {
449 let mint = asset.get("id").and_then(|value| value.as_str())?.to_string();
450
451 let name = asset
452 .pointer("/content/metadata/name")
453 .and_then(|value| value.as_str());
454
455 let symbol = asset
456 .pointer("/content/metadata/symbol")
457 .and_then(|value| value.as_str());
458
459 let token_info = asset.get("token_info").or_else(|| asset.pointer("/content/token_info"));
460
461 let decimals = token_info
462 .and_then(|info| info.get("decimals"))
463 .and_then(|value| value.as_u64());
464
465 let logo_uri = asset
466 .pointer("/content/links/image")
467 .and_then(|value| value.as_str())
468 .or_else(|| asset.pointer("/content/links/image_uri").and_then(|value| value.as_str()));
469
470 let mut obj = serde_json::Map::new();
471 obj.insert("mint".to_string(), serde_json::json!(mint));
472 obj.insert(
473 "name".to_string(),
474 name.map(|value| serde_json::json!(value))
475 .unwrap_or(Value::Null),
476 );
477 obj.insert(
478 "symbol".to_string(),
479 symbol.map(|value| serde_json::json!(value))
480 .unwrap_or(Value::Null),
481 );
482 obj.insert(
483 "decimals".to_string(),
484 decimals
485 .map(|value| serde_json::json!(value))
486 .unwrap_or(Value::Null),
487 );
488 obj.insert(
489 "logo_uri".to_string(),
490 logo_uri
491 .map(|value| serde_json::json!(value))
492 .unwrap_or(Value::Null),
493 );
494
495 Some((mint, Value::Object(obj)))
496 }
497}
498
499const DEFAULT_URL_TIMEOUT_SECS: u64 = 30;
504
505pub struct UrlResolverClient {
506 client: reqwest::Client,
507}
508
509impl Default for UrlResolverClient {
510 fn default() -> Self {
511 Self::new()
512 }
513}
514
515impl UrlResolverClient {
516 pub fn new() -> Self {
517 let client = reqwest::Client::builder()
518 .timeout(std::time::Duration::from_secs(DEFAULT_URL_TIMEOUT_SECS))
519 .build()
520 .expect("Failed to create HTTP client for URL resolver");
521
522 Self { client }
523 }
524
525 pub fn with_timeout(timeout_secs: u64) -> Self {
526 let client = reqwest::Client::builder()
527 .timeout(std::time::Duration::from_secs(timeout_secs))
528 .build()
529 .expect("Failed to create HTTP client for URL resolver");
530
531 Self { client }
532 }
533
534 pub async fn resolve(
536 &self,
537 url: &str,
538 method: &crate::ast::HttpMethod,
539 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
540 if url.is_empty() {
541 return Err("URL is empty".into());
542 }
543
544 let response = match method {
545 crate::ast::HttpMethod::Get => self.client.get(url).send().await?,
546 crate::ast::HttpMethod::Post => self.client.post(url).send().await?,
547 };
548
549 let response = response.error_for_status()?;
550 let value = response.json::<Value>().await?;
551
552 Ok(value)
553 }
554
555 pub async fn resolve_with_extract(
557 &self,
558 url: &str,
559 method: &crate::ast::HttpMethod,
560 extract_path: Option<&str>,
561 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
562 let response = self.resolve(url, method).await?;
563
564 if let Some(path) = extract_path {
565 Self::extract_json_path(&response, path)
566 } else {
567 Ok(response)
568 }
569 }
570
571 pub fn extract_json_path(
574 value: &Value,
575 path: &str,
576 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
577 if path.is_empty() {
578 return Ok(value.clone());
579 }
580
581 let mut current = value;
582 for segment in path.split('.') {
583 if let Some(next) = current.get(segment) {
585 current = next;
586 } else if let Ok(index) = segment.parse::<usize>() {
587 if let Some(next) = current.get(index) {
589 current = next;
590 } else {
591 return Err(format!("Index '{}' out of bounds in path '{}'", index, path).into());
592 }
593 } else {
594 return Err(format!("Key '{}' not found in path '{}'", segment, path).into());
595 }
596 }
597
598 Ok(current.clone())
599 }
600
601 pub async fn resolve_batch(
604 &self,
605 urls: &[(String, crate::ast::HttpMethod)],
606 ) -> HashMap<String, Value> {
607 let mut unique: HashMap<String, crate::ast::HttpMethod> = HashMap::new();
608 for (url, method) in urls {
609 if !url.is_empty() {
610 unique.entry(url.clone()).or_insert_with(|| method.clone());
611 }
612 }
613
614 let futures = unique
615 .into_iter()
616 .map(|(url, method)| async move {
617 let result = self.resolve(&url, &method).await;
618 (url, result)
619 });
620
621 join_all(futures)
622 .await
623 .into_iter()
624 .filter_map(|(url, result)| match result {
625 Ok(value) => Some((url, value)),
626 Err(e) => {
627 tracing::warn!(url = %url, error = %e, "Failed to resolve URL");
628 None
629 }
630 })
631 .collect()
632 }
633}
634
635struct ValidUrlRequest {
638 url: String,
639 method: crate::ast::HttpMethod,
640 request: crate::vm::ResolverRequest,
641}
642
643pub async fn resolve_url_batch(
649 vm: &std::sync::Mutex<crate::vm::VmContext>,
650 bytecode: &crate::compiler::MultiEntityBytecode,
651 url_client: &UrlResolverClient,
652 requests: Vec<crate::vm::ResolverRequest>,
653) -> Vec<crate::Mutation> {
654 if requests.is_empty() {
655 return Vec::new();
656 }
657
658 let mut valid = Vec::with_capacity(requests.len());
660 let mut invalid = Vec::new();
661
662 for request in requests {
663 if let crate::ast::ResolverType::Url(ref config) = request.resolver {
664 match &request.input {
665 serde_json::Value::String(s) if !s.is_empty() => {
666 valid.push(ValidUrlRequest {
667 url: s.clone(),
668 method: config.method.clone(),
669 request,
670 });
671 }
672 _ => {
673 tracing::warn!(
674 "URL resolver input is not a non-empty string: {:?}",
675 request.input
676 );
677 invalid.push(request);
678 }
679 }
680 }
681 }
682
683 if !invalid.is_empty() {
684 let mut vm = vm.lock().unwrap_or_else(|e| e.into_inner());
685 vm.restore_resolver_requests(invalid);
686 }
687
688 if valid.is_empty() {
689 return Vec::new();
690 }
691
692 let batch_input: Vec<_> = valid
694 .iter()
695 .map(|v| (v.url.clone(), v.method.clone()))
696 .collect();
697
698 let results = url_client.resolve_batch(&batch_input).await;
699
700 let mut vm = vm.lock().unwrap_or_else(|e| e.into_inner());
702 let mut mutations = Vec::new();
703 let mut failed = Vec::new();
704
705 for entry in valid {
706 match results.get(&entry.url) {
707 Some(resolved_value) => {
708 match vm.apply_resolver_result(bytecode, &entry.request.cache_key, resolved_value.clone()) {
709 Ok(mut new_mutations) => mutations.append(&mut new_mutations),
710 Err(err) => {
711 tracing::warn!(url = %entry.url, "Failed to apply URL resolver result: {}", err);
712 }
713 }
714 }
715 None => {
716 tracing::warn!(url = %entry.url, "URL resolver request failed, re-queuing");
717 failed.push(entry.request);
718 }
719 }
720 }
721
722 if !failed.is_empty() {
723 vm.restore_resolver_requests(failed);
724 }
725
726 mutations
727}
728
729struct TokenMetadataResolver;
730
731const TOKEN_METADATA_METHODS: &[ResolverComputedMethod] = &[
732 ResolverComputedMethod {
733 name: "ui_amount",
734 arg_count: 2,
735 },
736 ResolverComputedMethod {
737 name: "raw_amount",
738 arg_count: 2,
739 },
740];
741
742impl TokenMetadataResolver {
743 fn optional_f64(value: &Value) -> Option<f64> {
744 if value.is_null() {
745 return None;
746 }
747 match value {
748 Value::Number(number) => number.as_f64(),
749 Value::String(text) => text.parse::<f64>().ok(),
750 _ => None,
751 }
752 }
753
754 fn optional_u8(value: &Value) -> Option<u8> {
755 if value.is_null() {
756 return None;
757 }
758 match value {
759 Value::Number(number) => number
760 .as_u64()
761 .or_else(|| {
762 number
763 .as_i64()
764 .and_then(|v| if v >= 0 { Some(v as u64) } else { None })
765 })
766 .and_then(|v| u8::try_from(v).ok()),
767 Value::String(text) => text.parse::<u8>().ok(),
768 _ => None,
769 }
770 }
771
772 fn evaluate_ui_amount(
773 args: &[Value],
774 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
775 let raw_value = Self::optional_f64(&args[0]);
776 let decimals = Self::optional_u8(&args[1]);
777
778 match (raw_value, decimals) {
779 (Some(value), Some(decimals)) => {
780 let factor = 10_f64.powi(decimals as i32);
781 let result = value / factor;
782 if result.is_finite() {
783 serde_json::Number::from_f64(result)
784 .map(Value::Number)
785 .ok_or_else(|| "Failed to serialize ui_amount".into())
786 } else {
787 Err("ui_amount result is not finite".into())
788 }
789 }
790 _ => Ok(Value::Null),
791 }
792 }
793
794 fn evaluate_raw_amount(
795 args: &[Value],
796 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
797 let ui_value = Self::optional_f64(&args[0]);
798 let decimals = Self::optional_u8(&args[1]);
799
800 match (ui_value, decimals) {
801 (Some(value), Some(decimals)) => {
802 let factor = 10_f64.powi(decimals as i32);
803 let result = value * factor;
804 if !result.is_finite() || result < 0.0 {
805 return Err("raw_amount result is not finite".into());
806 }
807 let rounded = result.round();
808 if rounded > u64::MAX as f64 {
809 return Err("raw_amount result exceeds u64".into());
810 }
811 Ok(Value::Number(serde_json::Number::from(rounded as u64)))
812 }
813 _ => Ok(Value::Null),
814 }
815 }
816}
817
818impl ResolverDefinition for TokenMetadataResolver {
819 fn name(&self) -> &'static str {
820 "TokenMetadata"
821 }
822
823 fn output_type(&self) -> &'static str {
824 "TokenMetadata"
825 }
826
827 fn computed_methods(&self) -> &'static [ResolverComputedMethod] {
828 TOKEN_METADATA_METHODS
829 }
830
831 fn evaluate_computed(
832 &self,
833 method: &str,
834 args: &[Value],
835 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
836 match method {
837 "ui_amount" => Self::evaluate_ui_amount(args),
838 "raw_amount" => Self::evaluate_raw_amount(args),
839 _ => Err(format!("Unknown TokenMetadata method '{}'", method).into()),
840 }
841 }
842
843 fn typescript_interface(&self) -> Option<&'static str> {
844 Some(
845 r#"export interface TokenMetadata {
846 mint: string;
847 name?: string | null;
848 symbol?: string | null;
849 decimals?: number | null;
850 logo_uri?: string | null;
851}"#,
852 )
853 }
854
855 fn typescript_schema(&self) -> Option<ResolverTypeScriptSchema> {
856 Some(ResolverTypeScriptSchema {
857 name: "TokenMetadataSchema",
858 definition: r#"export const TokenMetadataSchema = z.object({
859 mint: z.string(),
860 name: z.string().nullable().optional(),
861 symbol: z.string().nullable().optional(),
862 decimals: z.number().nullable().optional(),
863 logo_uri: z.string().nullable().optional(),
864});"#,
865 })
866 }
867}
868
869impl<'a> InstructionContext<'a> {
870 pub fn new(
871 accounts: HashMap<String, String>,
872 state_id: u32,
873 reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
874 ) -> Self {
875 Self {
876 accounts,
877 state_id,
878 reverse_lookup_tx,
879 pending_updates: Vec::new(),
880 registers: None,
881 state_reg: None,
882 compiled_paths: None,
883 instruction_data: None,
884 slot: None,
885 signature: None,
886 timestamp: None,
887 dirty_tracker: crate::vm::DirtyTracker::new(),
888 }
889 }
890
891 #[allow(clippy::too_many_arguments)]
892 pub fn with_metrics(
893 accounts: HashMap<String, String>,
894 state_id: u32,
895 reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
896 registers: &'a mut Vec<crate::vm::RegisterValue>,
897 state_reg: crate::vm::Register,
898 compiled_paths: &'a HashMap<String, crate::metrics_context::CompiledPath>,
899 instruction_data: &'a serde_json::Value,
900 slot: Option<u64>,
901 signature: Option<String>,
902 timestamp: i64,
903 ) -> Self {
904 Self {
905 accounts,
906 state_id,
907 reverse_lookup_tx,
908 pending_updates: Vec::new(),
909 registers: Some(registers),
910 state_reg: Some(state_reg),
911 compiled_paths: Some(compiled_paths),
912 instruction_data: Some(instruction_data),
913 slot,
914 signature,
915 timestamp: Some(timestamp),
916 dirty_tracker: crate::vm::DirtyTracker::new(),
917 }
918 }
919
920 pub fn account(&self, name: &str) -> Option<String> {
922 self.accounts.get(name).cloned()
923 }
924
925 pub fn register_pda_reverse_lookup(&mut self, pda_address: &str, seed_value: &str) {
931 let pending = self
932 .reverse_lookup_tx
933 .update(pda_address.to_string(), seed_value.to_string());
934 self.pending_updates.extend(pending);
935 }
936
937 pub fn take_pending_updates(&mut self) -> Vec<crate::vm::PendingAccountUpdate> {
942 std::mem::take(&mut self.pending_updates)
943 }
944
945 pub fn dirty_tracker(&self) -> &crate::vm::DirtyTracker {
946 &self.dirty_tracker
947 }
948
949 pub fn dirty_tracker_mut(&mut self) -> &mut crate::vm::DirtyTracker {
950 &mut self.dirty_tracker
951 }
952
953 pub fn state_value(&self) -> Option<&serde_json::Value> {
955 if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
956 Some(®isters[state_reg])
957 } else {
958 None
959 }
960 }
961
962 pub fn get<T: serde::de::DeserializeOwned>(&self, field_path: &str) -> Option<T> {
965 if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
966 let state = ®isters[state_reg];
967 self.get_nested_value(state, field_path)
968 .and_then(|v| serde_json::from_value(v.clone()).ok())
969 } else {
970 None
971 }
972 }
973
974 pub fn set<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
975 if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
976 let serialized = serde_json::to_value(value).ok();
977 if let Some(val) = serialized {
978 Self::set_nested_value_static(&mut registers[state_reg], field_path, val);
979 self.dirty_tracker.mark_replaced(field_path);
980 println!(" ✓ Set field '{}' and marked as dirty", field_path);
981 }
982 } else {
983 println!(" ⚠️ Cannot set field '{}': metrics not configured (registers={}, state_reg={:?})",
984 field_path, self.registers.is_some(), self.state_reg);
985 }
986 }
987
988 pub fn increment(&mut self, field_path: &str, amount: i64) {
989 let current = self.get::<i64>(field_path).unwrap_or(0);
990 self.set(field_path, current + amount);
991 }
992
993 pub fn append<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
994 if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
995 let serialized = serde_json::to_value(&value).ok();
996 if let Some(val) = serialized {
997 Self::append_to_array_static(&mut registers[state_reg], field_path, val.clone());
998 self.dirty_tracker.mark_appended(field_path, val);
999 println!(
1000 " ✓ Appended to '{}' and marked as appended",
1001 field_path
1002 );
1003 }
1004 } else {
1005 println!(
1006 " ⚠️ Cannot append to '{}': metrics not configured",
1007 field_path
1008 );
1009 }
1010 }
1011
1012 fn append_to_array_static(
1013 value: &mut serde_json::Value,
1014 path: &str,
1015 new_value: serde_json::Value,
1016 ) {
1017 let segments: Vec<&str> = path.split('.').collect();
1018 if segments.is_empty() {
1019 return;
1020 }
1021
1022 let mut current = value;
1023 for segment in &segments[..segments.len() - 1] {
1024 if !current.is_object() {
1025 *current = serde_json::json!({});
1026 }
1027 let obj = current.as_object_mut().unwrap();
1028 current = obj
1029 .entry(segment.to_string())
1030 .or_insert(serde_json::json!({}));
1031 }
1032
1033 let last_segment = segments[segments.len() - 1];
1034 if !current.is_object() {
1035 *current = serde_json::json!({});
1036 }
1037 let obj = current.as_object_mut().unwrap();
1038 let arr = obj
1039 .entry(last_segment.to_string())
1040 .or_insert_with(|| serde_json::json!([]));
1041 if let Some(arr) = arr.as_array_mut() {
1042 arr.push(new_value);
1043 }
1044 }
1045
1046 fn get_nested_value<'b>(
1047 &self,
1048 value: &'b serde_json::Value,
1049 path: &str,
1050 ) -> Option<&'b serde_json::Value> {
1051 let mut current = value;
1052 for segment in path.split('.') {
1053 current = current.get(segment)?;
1054 }
1055 Some(current)
1056 }
1057
1058 fn set_nested_value_static(
1059 value: &mut serde_json::Value,
1060 path: &str,
1061 new_value: serde_json::Value,
1062 ) {
1063 let segments: Vec<&str> = path.split('.').collect();
1064 if segments.is_empty() {
1065 return;
1066 }
1067
1068 let mut current = value;
1069 for segment in &segments[..segments.len() - 1] {
1070 if !current.is_object() {
1071 *current = serde_json::json!({});
1072 }
1073 let obj = current.as_object_mut().unwrap();
1074 current = obj
1075 .entry(segment.to_string())
1076 .or_insert(serde_json::json!({}));
1077 }
1078
1079 if !current.is_object() {
1080 *current = serde_json::json!({});
1081 }
1082 if let Some(obj) = current.as_object_mut() {
1083 obj.insert(segments[segments.len() - 1].to_string(), new_value);
1084 }
1085 }
1086
1087 pub fn data<T: serde::de::DeserializeOwned>(&self, field: &str) -> Option<T> {
1089 self.instruction_data
1090 .and_then(|data| data.get(field))
1091 .and_then(|v| serde_json::from_value(v.clone()).ok())
1092 }
1093
1094 pub fn timestamp(&self) -> i64 {
1096 self.timestamp.unwrap_or(0)
1097 }
1098
1099 pub fn slot(&self) -> Option<u64> {
1101 self.slot
1102 }
1103
1104 pub fn signature(&self) -> Option<&str> {
1106 self.signature.as_deref()
1107 }
1108}