1use std::collections::{HashMap, HashSet};
2use std::sync::OnceLock;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7pub struct ResolveContext<'a> {
9 #[allow(dead_code)]
10 pub(crate) state_id: u32,
11 pub(crate) slot: u64,
12 pub(crate) signature: String,
13 pub(crate) reverse_lookups:
14 &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
15}
16
17impl<'a> ResolveContext<'a> {
18 pub fn new(
20 state_id: u32,
21 slot: u64,
22 signature: String,
23 reverse_lookups: &'a mut std::collections::HashMap<String, crate::vm::PdaReverseLookup>,
24 ) -> Self {
25 Self {
26 state_id,
27 slot,
28 signature,
29 reverse_lookups,
30 }
31 }
32
33 pub fn pda_reverse_lookup(&mut self, pda_address: &str) -> Option<String> {
36 let lookup_name = "default_pda_lookup";
37 self.reverse_lookups
38 .get_mut(lookup_name)
39 .and_then(|t| t.lookup(pda_address))
40 }
41
42 pub fn slot(&self) -> u64 {
43 self.slot
44 }
45
46 pub fn signature(&self) -> &str {
47 &self.signature
48 }
49}
50
51pub enum KeyResolution {
53 Found(String),
55
56 QueueUntil(&'static [u8]),
59
60 Skip,
62}
63
64pub struct InstructionContext<'a> {
66 pub(crate) accounts: HashMap<String, String>,
67 #[allow(dead_code)]
68 pub(crate) state_id: u32,
69 pub(crate) reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
70 pub(crate) pending_updates: Vec<crate::vm::PendingAccountUpdate>,
71 pub(crate) registers: Option<&'a mut Vec<crate::vm::RegisterValue>>,
72 pub(crate) state_reg: Option<crate::vm::Register>,
73 #[allow(dead_code)]
74 pub(crate) compiled_paths: Option<&'a HashMap<String, crate::metrics_context::CompiledPath>>,
75 pub(crate) instruction_data: Option<&'a serde_json::Value>,
76 pub(crate) slot: Option<u64>,
77 pub(crate) signature: Option<String>,
78 pub(crate) timestamp: Option<i64>,
79 pub(crate) dirty_tracker: crate::vm::DirtyTracker,
80}
81
82pub trait ReverseLookupUpdater {
83 fn update(
84 &mut self,
85 pda_address: String,
86 seed_value: String,
87 ) -> Vec<crate::vm::PendingAccountUpdate>;
88 fn flush_pending(&mut self, pda_address: &str) -> Vec<crate::vm::PendingAccountUpdate>;
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct TokenMetadata {
93 pub mint: String,
94 pub name: Option<String>,
95 pub symbol: Option<String>,
96 pub decimals: Option<u8>,
97 pub logo_uri: Option<String>,
98}
99
100#[derive(Debug, Clone, Copy)]
101pub struct ResolverTypeScriptSchema {
102 pub name: &'static str,
103 pub definition: &'static str,
104}
105
106#[derive(Debug, Clone, Copy)]
107pub struct ResolverComputedMethod {
108 pub name: &'static str,
109 pub arg_count: usize,
110}
111
112pub trait ResolverDefinition: Send + Sync {
113 fn name(&self) -> &'static str;
114 fn output_type(&self) -> &'static str;
115 fn computed_methods(&self) -> &'static [ResolverComputedMethod];
116 fn evaluate_computed(
117 &self,
118 method: &str,
119 args: &[Value],
120 ) -> std::result::Result<Value, Box<dyn std::error::Error>>;
121 fn typescript_interface(&self) -> Option<&'static str> {
122 None
123 }
124 fn typescript_schema(&self) -> Option<ResolverTypeScriptSchema> {
125 None
126 }
127}
128
129pub struct ResolverRegistry {
130 resolvers: HashMap<String, Box<dyn ResolverDefinition>>,
131}
132
133impl Default for ResolverRegistry {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl ResolverRegistry {
140 pub fn new() -> Self {
141 Self {
142 resolvers: HashMap::new(),
143 }
144 }
145
146 pub fn register(&mut self, resolver: Box<dyn ResolverDefinition>) {
147 self.resolvers.insert(resolver.name().to_string(), resolver);
148 }
149
150 pub fn resolver(&self, name: &str) -> Option<&dyn ResolverDefinition> {
151 self.resolvers.get(name).map(|resolver| resolver.as_ref())
152 }
153
154 pub fn definitions(&self) -> impl Iterator<Item = &dyn ResolverDefinition> {
155 self.resolvers.values().map(|resolver| resolver.as_ref())
156 }
157
158 pub fn is_output_type(&self, type_name: &str) -> bool {
159 self.resolvers
160 .values()
161 .any(|resolver| resolver.output_type() == type_name)
162 }
163
164 pub fn evaluate_computed(
165 &self,
166 resolver: &str,
167 method: &str,
168 args: &[Value],
169 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
170 let resolver_impl = self
171 .resolver(resolver)
172 .ok_or_else(|| format!("Unknown resolver '{}'", resolver))?;
173
174 let method_spec = resolver_impl
175 .computed_methods()
176 .iter()
177 .find(|spec| spec.name == method)
178 .ok_or_else(|| {
179 format!(
180 "Resolver '{}' does not provide method '{}'",
181 resolver, method
182 )
183 })?;
184
185 if method_spec.arg_count != args.len() {
186 return Err(format!(
187 "Resolver '{}' method '{}' expects {} args, got {}",
188 resolver,
189 method,
190 method_spec.arg_count,
191 args.len()
192 )
193 .into());
194 }
195
196 resolver_impl.evaluate_computed(method, args)
197 }
198
199 pub fn validate_computed_expr(
200 &self,
201 expr: &crate::ast::ComputedExpr,
202 errors: &mut Vec<String>,
203 ) {
204 match expr {
205 crate::ast::ComputedExpr::ResolverComputed {
206 resolver,
207 method,
208 args,
209 } => {
210 let resolver_impl = self.resolver(resolver);
211 if resolver_impl.is_none() {
212 errors.push(format!("Unknown resolver '{}'", resolver));
213 } else if let Some(resolver_impl) = resolver_impl {
214 let method_spec = resolver_impl
215 .computed_methods()
216 .iter()
217 .find(|spec| spec.name == method);
218 if let Some(method_spec) = method_spec {
219 if method_spec.arg_count != args.len() {
220 errors.push(format!(
221 "Resolver '{}' method '{}' expects {} args, got {}",
222 resolver,
223 method,
224 method_spec.arg_count,
225 args.len()
226 ));
227 }
228 } else {
229 errors.push(format!(
230 "Resolver '{}' does not provide method '{}'",
231 resolver, method
232 ));
233 }
234 }
235
236 for arg in args {
237 self.validate_computed_expr(arg, errors);
238 }
239 }
240 crate::ast::ComputedExpr::FieldRef { .. }
241 | crate::ast::ComputedExpr::Literal { .. }
242 | crate::ast::ComputedExpr::None
243 | crate::ast::ComputedExpr::Var { .. }
244 | crate::ast::ComputedExpr::ByteArray { .. }
245 | crate::ast::ComputedExpr::ContextSlot
246 | crate::ast::ComputedExpr::ContextTimestamp => {}
247 crate::ast::ComputedExpr::UnwrapOr { expr, .. }
248 | crate::ast::ComputedExpr::Cast { expr, .. }
249 | crate::ast::ComputedExpr::Paren { expr }
250 | crate::ast::ComputedExpr::Some { value: expr }
251 | crate::ast::ComputedExpr::Slice { expr, .. }
252 | crate::ast::ComputedExpr::Index { expr, .. }
253 | crate::ast::ComputedExpr::U64FromLeBytes { bytes: expr }
254 | crate::ast::ComputedExpr::U64FromBeBytes { bytes: expr }
255 | crate::ast::ComputedExpr::JsonToBytes { expr }
256 | crate::ast::ComputedExpr::Unary { expr, .. } => {
257 self.validate_computed_expr(expr, errors);
258 }
259 crate::ast::ComputedExpr::Binary { left, right, .. } => {
260 self.validate_computed_expr(left, errors);
261 self.validate_computed_expr(right, errors);
262 }
263 crate::ast::ComputedExpr::MethodCall { expr, args, .. } => {
264 self.validate_computed_expr(expr, errors);
265 for arg in args {
266 self.validate_computed_expr(arg, errors);
267 }
268 }
269 crate::ast::ComputedExpr::Let { value, body, .. } => {
270 self.validate_computed_expr(value, errors);
271 self.validate_computed_expr(body, errors);
272 }
273 crate::ast::ComputedExpr::If {
274 condition,
275 then_branch,
276 else_branch,
277 } => {
278 self.validate_computed_expr(condition, errors);
279 self.validate_computed_expr(then_branch, errors);
280 self.validate_computed_expr(else_branch, errors);
281 }
282 crate::ast::ComputedExpr::Closure { body, .. } => {
283 self.validate_computed_expr(body, errors);
284 }
285 }
286 }
287}
288
289static BUILTIN_RESOLVER_REGISTRY: OnceLock<ResolverRegistry> = OnceLock::new();
290
291pub fn register_builtin_resolvers(registry: &mut ResolverRegistry) {
292 registry.register(Box::new(TokenMetadataResolver));
293}
294
295pub fn builtin_resolver_registry() -> &'static ResolverRegistry {
296 BUILTIN_RESOLVER_REGISTRY.get_or_init(|| {
297 let mut registry = ResolverRegistry::new();
298 register_builtin_resolvers(&mut registry);
299 registry
300 })
301}
302
303pub fn evaluate_resolver_computed(
304 resolver: &str,
305 method: &str,
306 args: &[Value],
307) -> std::result::Result<Value, Box<dyn std::error::Error>> {
308 builtin_resolver_registry().evaluate_computed(resolver, method, args)
309}
310
311pub fn validate_resolver_computed_specs(
312 specs: &[crate::ast::ComputedFieldSpec],
313) -> std::result::Result<(), Box<dyn std::error::Error>> {
314 let registry = builtin_resolver_registry();
315 let mut errors = Vec::new();
316
317 for spec in specs {
318 registry.validate_computed_expr(&spec.expression, &mut errors);
319 }
320
321 if errors.is_empty() {
322 Ok(())
323 } else {
324 Err(errors.join("\n").into())
325 }
326}
327
328pub fn is_resolver_output_type(type_name: &str) -> bool {
329 builtin_resolver_registry().is_output_type(type_name)
330}
331
332const DEFAULT_DAS_BATCH_SIZE: usize = 100;
333const DEFAULT_DAS_TIMEOUT_SECS: u64 = 10;
334const DAS_API_ENDPOINT_ENV: &str = "DAS_API_ENDPOINT";
335const DAS_API_BATCH_ENV: &str = "DAS_API_BATCH_SIZE";
336
337pub struct TokenMetadataResolverClient {
338 endpoint: String,
339 client: reqwest::Client,
340 batch_size: usize,
341}
342
343impl TokenMetadataResolverClient {
344 pub fn new(
345 endpoint: String,
346 batch_size: usize,
347 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
348 let client = reqwest::Client::builder()
349 .timeout(std::time::Duration::from_secs(DEFAULT_DAS_TIMEOUT_SECS))
350 .build()?;
351
352 Ok(Self {
353 endpoint,
354 client,
355 batch_size: batch_size.max(1),
356 })
357 }
358
359 pub fn from_env(
360 ) -> Result<Option<Self>, Box<dyn std::error::Error + Send + Sync>> {
361 let Some(endpoint) = std::env::var(DAS_API_ENDPOINT_ENV).ok() else {
362 return Ok(None);
363 };
364
365 let batch_size = std::env::var(DAS_API_BATCH_ENV)
366 .ok()
367 .and_then(|value| value.parse::<usize>().ok())
368 .unwrap_or(DEFAULT_DAS_BATCH_SIZE);
369
370 Ok(Some(Self::new(endpoint, batch_size)?))
371 }
372
373 pub async fn resolve_token_metadata(
374 &self,
375 mints: &[String],
376 ) -> Result<HashMap<String, Value>, Box<dyn std::error::Error + Send + Sync>> {
377 let mut unique = HashSet::new();
378 let mut deduped = Vec::new();
379
380 for mint in mints {
381 if mint.is_empty() {
382 continue;
383 }
384 if unique.insert(mint.clone()) {
385 deduped.push(mint.clone());
386 }
387 }
388
389 let mut results = HashMap::new();
390 if deduped.is_empty() {
391 return Ok(results);
392 }
393
394 for chunk in deduped.chunks(self.batch_size) {
395 let assets = self.fetch_assets(chunk).await?;
396 for asset in assets {
397 if let Some((mint, value)) = Self::build_token_metadata(&asset) {
398 results.insert(mint, value);
399 }
400 }
401 }
402
403 Ok(results)
404 }
405
406 async fn fetch_assets(
407 &self,
408 ids: &[String],
409 ) -> Result<Vec<Value>, Box<dyn std::error::Error + Send + Sync>> {
410 let payload = serde_json::json!({
411 "jsonrpc": "2.0",
412 "id": "1",
413 "method": "getAssetBatch",
414 "params": {
415 "ids": ids,
416 "options": {
417 "showFungible": true,
418 },
419 },
420 });
421
422 let response = self.client.post(&self.endpoint).json(&payload).send().await?;
423 let response = response.error_for_status()?;
424 let value = response.json::<Value>().await?;
425
426 if let Some(error) = value.get("error") {
427 return Err(format!("Resolver response error: {}", error).into());
428 }
429
430 let assets = value
431 .get("result")
432 .and_then(|result| match result {
433 Value::Array(items) => Some(items.clone()),
434 Value::Object(obj) => obj
435 .get("items")
436 .and_then(|items| items.as_array())
437 .cloned(),
438 _ => None,
439 })
440 .ok_or_else(|| "Resolver response missing result".to_string())?;
441
442 let assets = assets.into_iter().filter(|a| !a.is_null()).collect();
443 Ok(assets)
444 }
445
446 fn build_token_metadata(asset: &Value) -> Option<(String, Value)> {
447 let mint = asset.get("id").and_then(|value| value.as_str())?.to_string();
448
449 let name = asset
450 .pointer("/content/metadata/name")
451 .and_then(|value| value.as_str());
452
453 let symbol = asset
454 .pointer("/content/metadata/symbol")
455 .and_then(|value| value.as_str());
456
457 let token_info = asset.get("token_info").or_else(|| asset.pointer("/content/token_info"));
458
459 let decimals = token_info
460 .and_then(|info| info.get("decimals"))
461 .and_then(|value| value.as_u64());
462
463 let logo_uri = asset
464 .pointer("/content/links/image")
465 .and_then(|value| value.as_str())
466 .or_else(|| asset.pointer("/content/links/image_uri").and_then(|value| value.as_str()));
467
468 let mut obj = serde_json::Map::new();
469 obj.insert("mint".to_string(), serde_json::json!(mint));
470 obj.insert(
471 "name".to_string(),
472 name.map(|value| serde_json::json!(value))
473 .unwrap_or(Value::Null),
474 );
475 obj.insert(
476 "symbol".to_string(),
477 symbol.map(|value| serde_json::json!(value))
478 .unwrap_or(Value::Null),
479 );
480 obj.insert(
481 "decimals".to_string(),
482 decimals
483 .map(|value| serde_json::json!(value))
484 .unwrap_or(Value::Null),
485 );
486 obj.insert(
487 "logo_uri".to_string(),
488 logo_uri
489 .map(|value| serde_json::json!(value))
490 .unwrap_or(Value::Null),
491 );
492
493 Some((mint, Value::Object(obj)))
494 }
495}
496
497struct TokenMetadataResolver;
498
499const TOKEN_METADATA_METHODS: &[ResolverComputedMethod] = &[
500 ResolverComputedMethod {
501 name: "ui_amount",
502 arg_count: 2,
503 },
504 ResolverComputedMethod {
505 name: "raw_amount",
506 arg_count: 2,
507 },
508];
509
510impl TokenMetadataResolver {
511 fn optional_f64(value: &Value) -> Option<f64> {
512 if value.is_null() {
513 return None;
514 }
515 match value {
516 Value::Number(number) => number.as_f64(),
517 Value::String(text) => text.parse::<f64>().ok(),
518 _ => None,
519 }
520 }
521
522 fn optional_u8(value: &Value) -> Option<u8> {
523 if value.is_null() {
524 return None;
525 }
526 match value {
527 Value::Number(number) => number
528 .as_u64()
529 .or_else(|| {
530 number
531 .as_i64()
532 .and_then(|v| if v >= 0 { Some(v as u64) } else { None })
533 })
534 .and_then(|v| u8::try_from(v).ok()),
535 Value::String(text) => text.parse::<u8>().ok(),
536 _ => None,
537 }
538 }
539
540 fn evaluate_ui_amount(
541 args: &[Value],
542 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
543 let raw_value = Self::optional_f64(&args[0]);
544 let decimals = Self::optional_u8(&args[1]);
545
546 match (raw_value, decimals) {
547 (Some(value), Some(decimals)) => {
548 let factor = 10_f64.powi(decimals as i32);
549 let result = value / factor;
550 if result.is_finite() {
551 serde_json::Number::from_f64(result)
552 .map(Value::Number)
553 .ok_or_else(|| "Failed to serialize ui_amount".into())
554 } else {
555 Err("ui_amount result is not finite".into())
556 }
557 }
558 _ => Ok(Value::Null),
559 }
560 }
561
562 fn evaluate_raw_amount(
563 args: &[Value],
564 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
565 let ui_value = Self::optional_f64(&args[0]);
566 let decimals = Self::optional_u8(&args[1]);
567
568 match (ui_value, decimals) {
569 (Some(value), Some(decimals)) => {
570 let factor = 10_f64.powi(decimals as i32);
571 let result = value * factor;
572 if !result.is_finite() || result < 0.0 {
573 return Err("raw_amount result is not finite".into());
574 }
575 let rounded = result.round();
576 if rounded > u64::MAX as f64 {
577 return Err("raw_amount result exceeds u64".into());
578 }
579 Ok(Value::Number(serde_json::Number::from(rounded as u64)))
580 }
581 _ => Ok(Value::Null),
582 }
583 }
584}
585
586impl ResolverDefinition for TokenMetadataResolver {
587 fn name(&self) -> &'static str {
588 "TokenMetadata"
589 }
590
591 fn output_type(&self) -> &'static str {
592 "TokenMetadata"
593 }
594
595 fn computed_methods(&self) -> &'static [ResolverComputedMethod] {
596 TOKEN_METADATA_METHODS
597 }
598
599 fn evaluate_computed(
600 &self,
601 method: &str,
602 args: &[Value],
603 ) -> std::result::Result<Value, Box<dyn std::error::Error>> {
604 match method {
605 "ui_amount" => Self::evaluate_ui_amount(args),
606 "raw_amount" => Self::evaluate_raw_amount(args),
607 _ => Err(format!("Unknown TokenMetadata method '{}'", method).into()),
608 }
609 }
610
611 fn typescript_interface(&self) -> Option<&'static str> {
612 Some(
613 r#"export interface TokenMetadata {
614 mint: string;
615 name?: string | null;
616 symbol?: string | null;
617 decimals?: number | null;
618 logo_uri?: string | null;
619}"#,
620 )
621 }
622
623 fn typescript_schema(&self) -> Option<ResolverTypeScriptSchema> {
624 Some(ResolverTypeScriptSchema {
625 name: "TokenMetadataSchema",
626 definition: r#"export const TokenMetadataSchema = z.object({
627 mint: z.string(),
628 name: z.string().nullable().optional(),
629 symbol: z.string().nullable().optional(),
630 decimals: z.number().nullable().optional(),
631 logo_uri: z.string().nullable().optional(),
632});"#,
633 })
634 }
635}
636
637impl<'a> InstructionContext<'a> {
638 pub fn new(
639 accounts: HashMap<String, String>,
640 state_id: u32,
641 reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
642 ) -> Self {
643 Self {
644 accounts,
645 state_id,
646 reverse_lookup_tx,
647 pending_updates: Vec::new(),
648 registers: None,
649 state_reg: None,
650 compiled_paths: None,
651 instruction_data: None,
652 slot: None,
653 signature: None,
654 timestamp: None,
655 dirty_tracker: crate::vm::DirtyTracker::new(),
656 }
657 }
658
659 #[allow(clippy::too_many_arguments)]
660 pub fn with_metrics(
661 accounts: HashMap<String, String>,
662 state_id: u32,
663 reverse_lookup_tx: &'a mut dyn ReverseLookupUpdater,
664 registers: &'a mut Vec<crate::vm::RegisterValue>,
665 state_reg: crate::vm::Register,
666 compiled_paths: &'a HashMap<String, crate::metrics_context::CompiledPath>,
667 instruction_data: &'a serde_json::Value,
668 slot: Option<u64>,
669 signature: Option<String>,
670 timestamp: i64,
671 ) -> Self {
672 Self {
673 accounts,
674 state_id,
675 reverse_lookup_tx,
676 pending_updates: Vec::new(),
677 registers: Some(registers),
678 state_reg: Some(state_reg),
679 compiled_paths: Some(compiled_paths),
680 instruction_data: Some(instruction_data),
681 slot,
682 signature,
683 timestamp: Some(timestamp),
684 dirty_tracker: crate::vm::DirtyTracker::new(),
685 }
686 }
687
688 pub fn account(&self, name: &str) -> Option<String> {
690 self.accounts.get(name).cloned()
691 }
692
693 pub fn register_pda_reverse_lookup(&mut self, pda_address: &str, seed_value: &str) {
699 let pending = self
700 .reverse_lookup_tx
701 .update(pda_address.to_string(), seed_value.to_string());
702 self.pending_updates.extend(pending);
703 }
704
705 pub fn take_pending_updates(&mut self) -> Vec<crate::vm::PendingAccountUpdate> {
710 std::mem::take(&mut self.pending_updates)
711 }
712
713 pub fn dirty_tracker(&self) -> &crate::vm::DirtyTracker {
714 &self.dirty_tracker
715 }
716
717 pub fn dirty_tracker_mut(&mut self) -> &mut crate::vm::DirtyTracker {
718 &mut self.dirty_tracker
719 }
720
721 pub fn state_value(&self) -> Option<&serde_json::Value> {
723 if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
724 Some(®isters[state_reg])
725 } else {
726 None
727 }
728 }
729
730 pub fn get<T: serde::de::DeserializeOwned>(&self, field_path: &str) -> Option<T> {
733 if let (Some(registers), Some(state_reg)) = (self.registers.as_ref(), self.state_reg) {
734 let state = ®isters[state_reg];
735 self.get_nested_value(state, field_path)
736 .and_then(|v| serde_json::from_value(v.clone()).ok())
737 } else {
738 None
739 }
740 }
741
742 pub fn set<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
743 if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
744 let serialized = serde_json::to_value(value).ok();
745 if let Some(val) = serialized {
746 Self::set_nested_value_static(&mut registers[state_reg], field_path, val);
747 self.dirty_tracker.mark_replaced(field_path);
748 println!(" ✓ Set field '{}' and marked as dirty", field_path);
749 }
750 } else {
751 println!(" ⚠️ Cannot set field '{}': metrics not configured (registers={}, state_reg={:?})",
752 field_path, self.registers.is_some(), self.state_reg);
753 }
754 }
755
756 pub fn increment(&mut self, field_path: &str, amount: i64) {
757 let current = self.get::<i64>(field_path).unwrap_or(0);
758 self.set(field_path, current + amount);
759 }
760
761 pub fn append<T: serde::Serialize>(&mut self, field_path: &str, value: T) {
762 if let (Some(registers), Some(state_reg)) = (self.registers.as_mut(), self.state_reg) {
763 let serialized = serde_json::to_value(&value).ok();
764 if let Some(val) = serialized {
765 Self::append_to_array_static(&mut registers[state_reg], field_path, val.clone());
766 self.dirty_tracker.mark_appended(field_path, val);
767 println!(
768 " ✓ Appended to '{}' and marked as appended",
769 field_path
770 );
771 }
772 } else {
773 println!(
774 " ⚠️ Cannot append to '{}': metrics not configured",
775 field_path
776 );
777 }
778 }
779
780 fn append_to_array_static(
781 value: &mut serde_json::Value,
782 path: &str,
783 new_value: serde_json::Value,
784 ) {
785 let segments: Vec<&str> = path.split('.').collect();
786 if segments.is_empty() {
787 return;
788 }
789
790 let mut current = value;
791 for segment in &segments[..segments.len() - 1] {
792 if !current.is_object() {
793 *current = serde_json::json!({});
794 }
795 let obj = current.as_object_mut().unwrap();
796 current = obj
797 .entry(segment.to_string())
798 .or_insert(serde_json::json!({}));
799 }
800
801 let last_segment = segments[segments.len() - 1];
802 if !current.is_object() {
803 *current = serde_json::json!({});
804 }
805 let obj = current.as_object_mut().unwrap();
806 let arr = obj
807 .entry(last_segment.to_string())
808 .or_insert_with(|| serde_json::json!([]));
809 if let Some(arr) = arr.as_array_mut() {
810 arr.push(new_value);
811 }
812 }
813
814 fn get_nested_value<'b>(
815 &self,
816 value: &'b serde_json::Value,
817 path: &str,
818 ) -> Option<&'b serde_json::Value> {
819 let mut current = value;
820 for segment in path.split('.') {
821 current = current.get(segment)?;
822 }
823 Some(current)
824 }
825
826 fn set_nested_value_static(
827 value: &mut serde_json::Value,
828 path: &str,
829 new_value: serde_json::Value,
830 ) {
831 let segments: Vec<&str> = path.split('.').collect();
832 if segments.is_empty() {
833 return;
834 }
835
836 let mut current = value;
837 for segment in &segments[..segments.len() - 1] {
838 if !current.is_object() {
839 *current = serde_json::json!({});
840 }
841 let obj = current.as_object_mut().unwrap();
842 current = obj
843 .entry(segment.to_string())
844 .or_insert(serde_json::json!({}));
845 }
846
847 if !current.is_object() {
848 *current = serde_json::json!({});
849 }
850 if let Some(obj) = current.as_object_mut() {
851 obj.insert(segments[segments.len() - 1].to_string(), new_value);
852 }
853 }
854
855 pub fn data<T: serde::de::DeserializeOwned>(&self, field: &str) -> Option<T> {
857 self.instruction_data
858 .and_then(|data| data.get(field))
859 .and_then(|v| serde_json::from_value(v.clone()).ok())
860 }
861
862 pub fn timestamp(&self) -> i64 {
864 self.timestamp.unwrap_or(0)
865 }
866
867 pub fn slot(&self) -> Option<u64> {
869 self.slot
870 }
871
872 pub fn signature(&self) -> Option<&str> {
874 self.signature.as_deref()
875 }
876}