1use std::future::Future;
5use std::pin::Pin;
6
7use num_bigint::{BigInt, BigUint, Sign};
8
9use crate::decoder::{ArgumentValue, DecodedArguments};
10use crate::error::Error;
11use crate::outcome::{render_warning, FormatDiagnostic, RenderDiagnosticKind, RenderState};
12use crate::path::{apply_collection_access, CollectionSelection};
13use crate::provider::DataProvider;
14use crate::render_shared::{
15 chain_name, coerce_unsigned_biguint_from_argument_value,
16 coerce_unsigned_decimal_string_from_argument_value, format_blockheight_timestamp,
17 format_duration_seconds, format_timestamp, format_token_amount_output, format_unit_biguint,
18 format_with_decimals, is_excluded_path, lookup_map_entry, native_token_meta,
19 resolve_interpolation_field_spec, resolve_metadata_constant_str,
20};
21use crate::resolver::ResolvedDescriptor;
22use crate::types::descriptor::Descriptor;
23use crate::types::display::{
24 DisplayField, DisplayFormat, FieldFormat, FieldGroup, FormatParams, Iteration, SenderAddress,
25 UintLiteral, VisibleLiteral, VisibleRule,
26};
27
28const MAX_CALLDATA_DEPTH: u8 = 3;
30
31#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
33#[derive(Debug, Clone, serde::Serialize)]
34pub struct DisplayModel {
35 pub intent: String,
36 pub interpolated_intent: Option<String>,
37 pub entries: Vec<DisplayEntry>,
38 pub owner: Option<String>,
40 pub contract_name: Option<String>,
41}
42
43#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
45#[derive(Debug, Clone, serde::Serialize)]
46pub enum DisplayEntry {
47 Item(DisplayItem),
48 Group {
49 label: String,
50 iteration: GroupIteration,
51 items: Vec<DisplayItem>,
52 },
53 Nested {
54 label: String,
55 intent: String,
56 entries: Vec<DisplayEntry>,
57 },
58}
59
60#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
61#[derive(Debug, Clone, serde::Serialize)]
62pub enum GroupIteration {
63 Sequential,
64 Bundled,
65}
66
67#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
69#[derive(Debug, Clone, serde::Serialize)]
70pub struct DisplayItem {
71 pub label: String,
72 pub value: String,
73}
74
75struct RenderContext<'a> {
77 descriptor: &'a Descriptor,
78 decoded: &'a DecodedArguments,
79 chain_id: u64,
80 data_provider: &'a dyn DataProvider,
81 descriptors: &'a [ResolvedDescriptor],
82 depth: u8,
83}
84
85type RenderDiagnostics = Vec<FormatDiagnostic>;
86
87#[allow(clippy::too_many_arguments)]
91pub(crate) async fn format_calldata(
92 descriptor: &Descriptor,
93 chain_id: u64,
94 _to: &str,
95 decoded: &DecodedArguments,
96 _value: Option<&[u8]>,
97 data_provider: &dyn DataProvider,
98 descriptors: &[ResolvedDescriptor],
99 state: &mut RenderState,
100) -> Result<DisplayModel, Error> {
101 let format = find_format(descriptor, &decoded.function_name, &decoded.selector)?;
103
104 let ctx = RenderContext {
105 descriptor,
106 decoded,
107 chain_id,
108 data_provider,
109 descriptors,
110 depth: 0,
111 };
112
113 let mut warnings = RenderDiagnostics::new();
114 let mut nested_fallback = false;
115 let expanded_fields = expand_display_fields(descriptor, &format.fields, &mut warnings);
116 let entries =
117 render_fields(&ctx, &expanded_fields, &mut warnings, &mut nested_fallback).await?;
118
119 let interpolated = match format.interpolated_intent.as_ref() {
120 Some(template) => {
121 match interpolate_intent(template, &ctx, &expanded_fields, &format.excluded).await {
122 Ok(rendered) => Some(rendered),
123 Err(err) => {
124 warnings.push(render_warning(
125 RenderDiagnosticKind::InterpolatedIntentSkipped,
126 format!("interpolated intent skipped: {err}"),
127 ));
128 None
129 }
130 }
131 }
132 None => None,
133 };
134
135 let model = DisplayModel {
136 intent: format
137 .intent
138 .as_ref()
139 .map(crate::types::display::intent_as_string)
140 .unwrap_or_else(|| decoded.function_name.clone()),
141 interpolated_intent: interpolated,
142 entries,
143 owner: descriptor.metadata.owner.clone(),
144 contract_name: descriptor.metadata.contract_name.clone(),
145 };
146
147 record_diagnostics(state, &warnings);
148 if nested_fallback {
149 state.mark_nested_fallback();
150 }
151
152 Ok(model)
153}
154
155fn find_format<'a>(
160 descriptor: &'a Descriptor,
161 function_name: &str,
162 selector: &[u8; 4],
163) -> Result<&'a DisplayFormat, Error> {
164 let selector_hex = hex::encode(selector);
165 let mut matches: Vec<(&str, &'a DisplayFormat)> = Vec::new();
166
167 for (key, format) in &descriptor.display.formats {
168 if key == function_name {
169 matches.push((key, format));
170 continue;
171 }
172 if key.contains('(') {
173 if let Ok(parsed) = crate::decoder::parse_signature(key) {
174 if hex::encode(parsed.selector) == selector_hex {
175 matches.push((key, format));
176 }
177 }
178 }
179 }
180
181 match matches.len() {
182 0 => Err(Error::Render(format!(
183 "no display format found for function '{}' (selector 0x{})",
184 function_name, selector_hex
185 ))),
186 1 => Ok(matches[0].1),
187 _ => {
188 let keys: Vec<&str> = matches.iter().map(|(k, _)| *k).collect();
189 Err(Error::Descriptor(format!(
190 "duplicate selectors (0x{}) found for keys: {}",
191 selector_hex,
192 keys.join(", ")
193 )))
194 }
195 }
196}
197
198fn render_fields<'a>(
202 ctx: &'a RenderContext<'a>,
203 fields: &[DisplayField],
204 warnings: &'a mut RenderDiagnostics,
205 nested_fallback: &'a mut bool,
206) -> Pin<Box<dyn Future<Output = Result<Vec<DisplayEntry>, Error>> + Send + 'a>> {
207 let fields = fields.to_vec();
208 Box::pin(async move {
209 let mut entries = Vec::new();
210
211 for field in &fields {
212 match field {
213 DisplayField::Group { field_group } => {
214 entries.extend(
215 render_field_group_entries(ctx, field_group, warnings, nested_fallback)
216 .await?,
217 );
218 }
219 DisplayField::Simple {
220 path,
221 label,
222 value: literal_value,
223 format,
224 params,
225 separator,
226 visible,
227 } => {
228 if let Some(lit) = literal_value {
230 if !check_visibility(visible, &None, label, "")? {
231 continue;
232 }
233 let resolved = resolve_metadata_constant_str(ctx.descriptor, lit);
234 entries.push(DisplayEntry::Item(DisplayItem {
235 label: label.clone(),
236 value: resolved,
237 }));
238 continue;
239 }
240
241 let path_str = path.as_deref().unwrap_or("");
242
243 if let Some((base, rest)) = split_array_iter_path(path_str) {
245 if let Some(ArgumentValue::Array(items)) = resolve_path(ctx.decoded, base) {
246 for item in &items {
247 let val = if rest.is_empty() {
248 Some(item.clone())
249 } else {
250 let rest_segments: Vec<&str> = rest.split('.').collect();
251 navigate_value(item, &rest_segments)
252 };
253 if !check_visibility(visible, &val, label, path_str)? {
254 continue;
255 }
256 let formatted = format_value(
257 ctx,
258 &val,
259 format.as_ref(),
260 params.as_ref(),
261 path_str,
262 label,
263 separator.as_deref(),
264 warnings,
265 )
266 .await?;
267 entries.push(DisplayEntry::Item(DisplayItem {
268 label: label.clone(),
269 value: formatted,
270 }));
271 }
272 continue;
273 }
274 }
275
276 let value = resolve_path(ctx.decoded, path_str);
278
279 if !check_visibility(visible, &value, label, path_str)? {
281 continue;
282 }
283
284 if let Some(fmt) = find_current_format(ctx) {
286 if is_excluded_path(&fmt.excluded, path_str) {
287 continue;
288 }
289 }
290
291 if matches!(format.as_ref(), Some(FieldFormat::Calldata)) {
293 let entry = render_calldata_field(
294 ctx,
295 &value,
296 params.as_ref(),
297 label,
298 warnings,
299 nested_fallback,
300 )
301 .await?;
302 entries.push(entry);
303 continue;
304 }
305
306 let formatted = format_value(
307 ctx,
308 &value,
309 format.as_ref(),
310 params.as_ref(),
311 path_str,
312 label,
313 separator.as_deref(),
314 warnings,
315 )
316 .await?;
317
318 entries.push(DisplayEntry::Item(DisplayItem {
319 label: label.clone(),
320 value: formatted,
321 }));
322 }
323 DisplayField::Reference { .. } | DisplayField::Scope { .. } => {
324 warnings.push(render_warning(
325 RenderDiagnosticKind::GenericRenderWarning,
326 "unexpanded display field reached renderer; skipping",
327 ));
328 }
329 }
330 }
331
332 Ok(entries)
333 })
334}
335
336enum GroupRenderKind {
337 Scalar(Vec<DisplayItem>),
338 Bundles(Vec<Vec<DisplayItem>>),
339}
340
341pub(crate) fn flatten_display_entry(entry: DisplayEntry) -> Vec<DisplayItem> {
342 match entry {
343 DisplayEntry::Item(item) => vec![item],
344 DisplayEntry::Group { items, .. } => items,
345 DisplayEntry::Nested { intent, .. } => {
346 vec![DisplayItem {
347 label: "Nested call".to_string(),
348 value: intent,
349 }]
350 }
351 }
352}
353
354fn render_group_field_kind<'a>(
355 ctx: &'a RenderContext<'a>,
356 field: &'a DisplayField,
357 warnings: &'a mut RenderDiagnostics,
358 nested_fallback: &'a mut bool,
359) -> Pin<Box<dyn Future<Output = Result<GroupRenderKind, Error>> + Send + 'a>> {
360 Box::pin(async move {
361 match field {
362 DisplayField::Group { field_group } => {
363 render_group_kind(ctx, field_group, warnings, nested_fallback).await
364 }
365 DisplayField::Simple {
366 path,
367 label,
368 value: literal_value,
369 format,
370 params,
371 separator,
372 visible,
373 } => {
374 if let Some(lit) = literal_value {
375 if !check_visibility(visible, &None, label, "")? {
376 return Ok(GroupRenderKind::Scalar(Vec::new()));
377 }
378 return Ok(GroupRenderKind::Scalar(vec![DisplayItem {
379 label: label.clone(),
380 value: resolve_metadata_constant_str(ctx.descriptor, lit),
381 }]));
382 }
383
384 let path_str = path.as_deref().unwrap_or("");
385 if let Some((base, rest)) = split_array_iter_path(path_str) {
386 if let Some(ArgumentValue::Array(items)) = resolve_path(ctx.decoded, base) {
387 let mut bundles = Vec::new();
388 for item in &items {
389 let val = if rest.is_empty() {
390 Some(item.clone())
391 } else {
392 let rest_segments: Vec<&str> = rest.split('.').collect();
393 navigate_value(item, &rest_segments)
394 };
395 if !check_visibility(visible, &val, label, path_str)? {
396 continue;
397 }
398 let rendered = if matches!(format.as_ref(), Some(FieldFormat::Calldata))
399 {
400 flatten_display_entry(
401 render_calldata_field(
402 ctx,
403 &val,
404 params.as_ref(),
405 label,
406 warnings,
407 nested_fallback,
408 )
409 .await?,
410 )
411 } else {
412 vec![DisplayItem {
413 label: label.clone(),
414 value: format_value(
415 ctx,
416 &val,
417 format.as_ref(),
418 params.as_ref(),
419 path_str,
420 label,
421 separator.as_deref(),
422 warnings,
423 )
424 .await?,
425 }]
426 };
427 bundles.push(rendered);
428 }
429 return Ok(GroupRenderKind::Bundles(bundles));
430 }
431 }
432
433 let value = resolve_path(ctx.decoded, path_str);
434 if !check_visibility(visible, &value, label, path_str)? {
435 return Ok(GroupRenderKind::Scalar(Vec::new()));
436 }
437
438 if matches!(format.as_ref(), Some(FieldFormat::Calldata)) {
439 return Ok(GroupRenderKind::Scalar(flatten_display_entry(
440 render_calldata_field(
441 ctx,
442 &value,
443 params.as_ref(),
444 label,
445 warnings,
446 nested_fallback,
447 )
448 .await?,
449 )));
450 }
451
452 Ok(GroupRenderKind::Scalar(vec![DisplayItem {
453 label: label.clone(),
454 value: format_value(
455 ctx,
456 &value,
457 format.as_ref(),
458 params.as_ref(),
459 path_str,
460 label,
461 separator.as_deref(),
462 warnings,
463 )
464 .await?,
465 }]))
466 }
467 DisplayField::Reference { .. } | DisplayField::Scope { .. } => {
468 Ok(GroupRenderKind::Scalar(Vec::new()))
469 }
470 }
471 })
472}
473
474fn render_group_kind<'a>(
475 ctx: &'a RenderContext<'a>,
476 group: &'a FieldGroup,
477 warnings: &'a mut RenderDiagnostics,
478 nested_fallback: &'a mut bool,
479) -> Pin<Box<dyn Future<Output = Result<GroupRenderKind, Error>> + Send + 'a>> {
480 Box::pin(async move {
481 let mut child_kinds = Vec::new();
482 for field in &group.fields {
483 child_kinds.push(render_group_field_kind(ctx, field, warnings, nested_fallback).await?);
484 }
485
486 match group.iteration {
487 Iteration::Sequential => {
488 let items = child_kinds
489 .into_iter()
490 .flat_map(|kind| match kind {
491 GroupRenderKind::Scalar(items) => items,
492 GroupRenderKind::Bundles(bundles) => {
493 bundles.into_iter().flatten().collect()
494 }
495 })
496 .collect();
497 Ok(GroupRenderKind::Scalar(items))
498 }
499 Iteration::Bundled => {
500 let mut bundle_sets = Vec::new();
501 for kind in child_kinds {
502 match kind {
503 GroupRenderKind::Bundles(bundles) => bundle_sets.push(bundles),
504 GroupRenderKind::Scalar(_) => {
505 return Err(Error::Render(
506 "bundled groups cannot mix array-expanded and scalar fields"
507 .to_string(),
508 ));
509 }
510 }
511 }
512
513 if bundle_sets.is_empty() {
514 return Ok(GroupRenderKind::Bundles(Vec::new()));
515 }
516
517 let expected_len = bundle_sets[0].len();
518 if bundle_sets
519 .iter()
520 .any(|bundles| bundles.len() != expected_len)
521 {
522 return Err(Error::Render(
523 "bundled groups require all array-expanded fields to have the same length"
524 .to_string(),
525 ));
526 }
527
528 let mut bundled = vec![Vec::new(); expected_len];
529 for bundles in bundle_sets {
530 for (index, items) in bundles.into_iter().enumerate() {
531 bundled[index].extend(items);
532 }
533 }
534 Ok(GroupRenderKind::Bundles(bundled))
535 }
536 }
537 })
538}
539
540async fn render_field_group_entries<'a>(
542 ctx: &'a RenderContext<'a>,
543 group: &FieldGroup,
544 warnings: &'a mut RenderDiagnostics,
545 nested_fallback: &'a mut bool,
546) -> Result<Vec<DisplayEntry>, Error> {
547 let rendered = render_group_kind(ctx, group, warnings, nested_fallback).await?;
548 match rendered {
549 GroupRenderKind::Scalar(items) => {
550 if items.is_empty() {
551 return Ok(Vec::new());
552 }
553 if let Some(label) = group.label.as_ref() {
554 Ok(vec![DisplayEntry::Group {
555 label: label.clone(),
556 iteration: GroupIteration::Sequential,
557 items,
558 }])
559 } else {
560 Ok(items.into_iter().map(DisplayEntry::Item).collect())
561 }
562 }
563 GroupRenderKind::Bundles(bundles) => {
564 let items: Vec<DisplayItem> = bundles.into_iter().flatten().collect();
565 if items.is_empty() {
566 return Ok(Vec::new());
567 }
568 Ok(vec![DisplayEntry::Group {
569 label: group.label.clone().unwrap_or_default(),
570 iteration: GroupIteration::Bundled,
571 items,
572 }])
573 }
574 }
575}
576
577fn resolve_reference(descriptor: &Descriptor, reference: &str) -> Option<DisplayField> {
582 let key = reference
583 .strip_prefix("$.display.definitions.")
584 .or_else(|| reference.strip_prefix("#/definitions/"))?;
585 descriptor.display.definitions.get(key).cloned()
586}
587
588pub(crate) fn expand_display_fields(
590 descriptor: &Descriptor,
591 fields: &[DisplayField],
592 warnings: &mut RenderDiagnostics,
593) -> Vec<DisplayField> {
594 let mut expanded = Vec::new();
595
596 for field in fields {
597 match field {
598 DisplayField::Reference {
599 reference,
600 path,
601 params: ref_params,
602 visible,
603 } => {
604 if let Some(resolved) = resolve_reference(descriptor, reference) {
605 let merged = merge_ref_with_definition(resolved, path, ref_params, visible);
606 expanded.extend(expand_display_fields(descriptor, &[merged], warnings));
607 } else {
608 warnings.push(render_warning(
609 RenderDiagnosticKind::DefinitionReferenceUnresolved,
610 format!("unresolved reference: {reference}"),
611 ));
612 }
613 }
614 DisplayField::Group { field_group } => {
615 let scoped_children = if let Some(scope_path) = field_group.path.as_deref() {
616 field_group
617 .fields
618 .iter()
619 .map(|field| prepend_scope_path(field, scope_path))
620 .collect()
621 } else {
622 field_group.fields.clone()
623 };
624 expanded.push(DisplayField::Group {
625 field_group: FieldGroup {
626 path: None,
627 label: field_group.label.clone(),
628 iteration: field_group.iteration.clone(),
629 fields: expand_display_fields(descriptor, &scoped_children, warnings),
630 },
631 });
632 }
633 DisplayField::Scope {
634 path: scope_path,
635 label,
636 iteration,
637 fields: children,
638 } => {
639 let scoped_children = if let Some(scope_path) = scope_path.as_deref() {
640 children
641 .iter()
642 .map(|child| prepend_scope_path(child, scope_path))
643 .collect()
644 } else {
645 children.clone()
646 };
647 expanded.push(DisplayField::Group {
648 field_group: FieldGroup {
649 path: None,
650 label: label.clone(),
651 iteration: iteration.clone(),
652 fields: expand_display_fields(descriptor, &scoped_children, warnings),
653 },
654 });
655 }
656 DisplayField::Simple { .. } => expanded.push(field.clone()),
657 }
658 }
659
660 expanded
661}
662
663pub fn prepend_scope_path(field: &DisplayField, scope: &str) -> DisplayField {
668 match field {
669 DisplayField::Reference {
670 reference,
671 path,
672 params,
673 visible,
674 } => DisplayField::Reference {
675 reference: reference.clone(),
676 path: Some(prepend_path(scope, path.as_deref())),
677 params: params.as_ref().map(|p| prepend_params(scope, p)),
678 visible: visible.clone(),
679 },
680 DisplayField::Group { field_group } => DisplayField::Group {
681 field_group: FieldGroup {
682 path: field_group
683 .path
684 .as_deref()
685 .map(|path| prepend_path(scope, Some(path))),
686 label: field_group.label.clone(),
687 iteration: field_group.iteration.clone(),
688 fields: field_group.fields.clone(),
689 },
690 },
691 DisplayField::Scope {
692 path,
693 label,
694 iteration,
695 fields: children,
696 } => DisplayField::Scope {
697 path: Some(prepend_path(scope, path.as_deref())),
698 label: label.clone(),
699 iteration: iteration.clone(),
700 fields: children.clone(),
701 },
702 DisplayField::Simple {
703 path,
704 label,
705 value,
706 format,
707 params,
708 separator,
709 visible,
710 } => DisplayField::Simple {
711 path: Some(prepend_path(scope, path.as_deref())),
712 label: label.clone(),
713 value: value.clone(),
714 format: format.clone(),
715 params: params.as_ref().map(|p| prepend_params(scope, p)),
716 separator: separator.clone(),
717 visible: visible.clone(),
718 },
719 }
720}
721
722fn prepend_path(scope: &str, child: Option<&str>) -> String {
724 match child {
725 Some(p) if !p.is_empty() => format!("{scope}.{p}"),
726 _ => scope.to_string(),
727 }
728}
729
730fn prepend_params(scope: &str, params: &FormatParams) -> FormatParams {
732 let mut p = params.clone();
733 if let Some(ref tp) = p.token_path {
735 if !tp.starts_with('#') && !tp.starts_with("@.") {
736 p.token_path = Some(format!("{scope}.{tp}"));
737 }
738 }
739 p
740}
741
742pub fn merge_ref_with_definition(
747 definition: DisplayField,
748 ref_path: &Option<String>,
749 ref_params: &Option<FormatParams>,
750 ref_visible: &VisibleRule,
751) -> DisplayField {
752 match definition {
753 DisplayField::Simple {
754 path: def_path,
755 label,
756 value,
757 format,
758 params: def_params,
759 separator,
760 visible: _,
761 } => {
762 let path = ref_path.clone().or(def_path);
764
765 let params = match (def_params, ref_params) {
767 (None, None) => None,
768 (Some(dp), None) => Some(dp),
769 (None, Some(rp)) => Some(rp.clone()),
770 (Some(mut dp), Some(rp)) => {
771 if let Some(v) = &rp.token_path {
773 dp.token_path = Some(v.clone());
774 }
775 if let Some(v) = &rp.native_currency_address {
776 dp.native_currency_address = Some(v.clone());
777 }
778 if let Some(v) = &rp.threshold {
779 dp.threshold = Some(v.clone());
780 }
781 if let Some(v) = &rp.message {
782 dp.message = Some(v.clone());
783 }
784 if let Some(v) = &rp.ref_path {
785 dp.ref_path = Some(v.clone());
786 }
787 if let Some(v) = &rp.callee_path {
788 dp.callee_path = Some(v.clone());
789 }
790 if let Some(v) = &rp.amount_path {
791 dp.amount_path = Some(v.clone());
792 }
793 if let Some(v) = &rp.spender_path {
794 dp.spender_path = Some(v.clone());
795 }
796 if let Some(v) = &rp.selector_path {
797 dp.selector_path = Some(v.clone());
798 }
799 if let Some(v) = &rp.chain_id_path {
800 dp.chain_id_path = Some(v.clone());
801 }
802 if let Some(v) = &rp.encoding {
803 dp.encoding = Some(v.clone());
804 }
805 if rp.prefix.is_some() {
806 dp.prefix = rp.prefix;
807 }
808 if let Some(v) = &rp.base {
809 dp.base = Some(v.clone());
810 }
811 if rp.decimals.is_some() {
812 dp.decimals = rp.decimals;
813 }
814 if let Some(v) = &rp.types {
815 dp.types = Some(v.clone());
816 }
817 if let Some(v) = &rp.sources {
818 dp.sources = Some(v.clone());
819 }
820 if let Some(v) = &rp.map_reference {
821 dp.map_reference = Some(v.clone());
822 }
823 if let Some(v) = &rp.enum_path {
824 dp.enum_path = Some(v.clone());
825 }
826 if rp.chain_id.is_some() {
827 dp.chain_id = rp.chain_id;
828 }
829 if let Some(v) = &rp.sender_address {
830 dp.sender_address = Some(v.clone());
831 }
832 if let Some(v) = &rp.collection_path {
833 dp.collection_path = Some(v.clone());
834 }
835 if let Some(v) = &rp.collection {
836 dp.collection = Some(v.clone());
837 }
838 if let Some(v) = &rp.encryption {
839 dp.encryption = Some(v.clone());
840 }
841 Some(dp)
842 }
843 };
844
845 DisplayField::Simple {
846 path,
847 label,
848 value,
849 format,
850 params,
851 separator,
852 visible: ref_visible.clone(),
853 }
854 }
855 other => other,
857 }
858}
859
860pub(crate) fn resolve_path(decoded: &DecodedArguments, path: &str) -> Option<ArgumentValue> {
866 let path = path.trim();
867
868 let path = path.strip_prefix("#.").unwrap_or(path);
870
871 let (prefer_container, path) = if let Some(stripped) = path.strip_prefix("@.") {
873 (true, stripped)
874 } else {
875 (false, path)
876 };
877
878 if let Ok(index) = path.parse::<usize>() {
880 return decoded.args.get(index).map(|a| a.value.clone());
881 }
882
883 let segments: Vec<&str> = path.split('.').collect();
885
886 if let Ok(index) = segments[0].parse::<usize>() {
888 if let Some(arg) = decoded.args.get(index) {
889 if segments.len() == 1 {
890 return Some(arg.value.clone());
891 }
892 return navigate_value(&arg.value, &segments[1..]);
893 }
894 }
895
896 if let Some(rest) = segments[0].strip_prefix("args") {
898 if let Some(idx_str) = rest.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
899 if let Ok(index) = idx_str.parse::<usize>() {
900 if let Some(arg) = decoded.args.get(index) {
901 if segments.len() == 1 {
902 return Some(arg.value.clone());
903 }
904 return navigate_value(&arg.value, &segments[1..]);
905 }
906 }
907 }
908 }
909
910 let name = segments[0];
913 let arg = if prefer_container {
914 decoded
915 .args
916 .iter()
917 .rfind(|a| a.name.as_deref() == Some(name))
918 } else {
919 decoded
920 .args
921 .iter()
922 .find(|a| a.name.as_deref() == Some(name))
923 };
924
925 if let Some(arg) = arg {
926 if segments.len() == 1 {
927 return Some(arg.value.clone());
928 }
929 return navigate_value(&arg.value, &segments[1..]);
930 }
931
932 None
933}
934
935fn navigate_value(value: &ArgumentValue, segments: &[&str]) -> Option<ArgumentValue> {
939 if segments.is_empty() {
940 return Some(value.clone());
941 }
942
943 match value {
944 ArgumentValue::Tuple(members) => {
945 let seg = segments[0];
946
947 if let Ok(index) = seg.parse::<usize>() {
949 return members
950 .get(index)
951 .and_then(|(_, v)| navigate_value(v, &segments[1..]));
952 }
953
954 members
956 .iter()
957 .find(|(name, _)| name.as_deref() == Some(seg))
958 .and_then(|(_, v)| navigate_value(v, &segments[1..]))
959 }
960 ArgumentValue::Array(members) => {
961 let seg = segments[0];
962 match apply_collection_access(members, seg)? {
963 CollectionSelection::Item(item) => navigate_value(&item, &segments[1..]),
964 CollectionSelection::Slice(slice) => {
965 navigate_value(&ArgumentValue::Array(slice), &segments[1..])
966 }
967 }
968 }
969 ArgumentValue::Bytes(bytes)
970 | ArgumentValue::FixedBytes(bytes)
971 | ArgumentValue::Uint(bytes)
972 | ArgumentValue::Int(bytes) => {
973 let seg = segments[0];
974 match apply_collection_access(bytes, seg)? {
975 CollectionSelection::Item(byte) => {
976 navigate_value(&ArgumentValue::Bytes(vec![byte]), &segments[1..])
977 }
978 CollectionSelection::Slice(slice) => {
979 navigate_value(&ArgumentValue::Bytes(slice), &segments[1..])
980 }
981 }
982 }
983 _ => None,
984 }
985}
986
987pub(crate) fn split_array_iter_path(path: &str) -> Option<(&str, &str)> {
994 let marker = ".[]";
995 let pos = path.find(marker)?;
996 let base = &path[..pos];
997 let rest = &path[pos + marker.len()..];
998 let rest = rest.strip_prefix('.').unwrap_or(rest);
1000 Some((base, rest))
1001}
1002
1003fn visibility_context(label: &str, path: &str) -> String {
1004 if path.is_empty() {
1005 format!("field '{}'", label)
1006 } else {
1007 format!("field '{}' (path '{}')", label, path)
1008 }
1009}
1010
1011fn check_visibility(
1013 rule: &VisibleRule,
1014 value: &Option<ArgumentValue>,
1015 label: &str,
1016 path: &str,
1017) -> Result<bool, Error> {
1018 match rule {
1019 VisibleRule::Always => Ok(true),
1020 VisibleRule::Bool(b) => Ok(*b),
1021 VisibleRule::Named(literal) => Ok(matches!(
1022 literal,
1023 VisibleLiteral::Always | VisibleLiteral::Optional
1024 )),
1025 VisibleRule::Condition(cond) => {
1026 let Some(val) = value else {
1027 if cond.must_match.is_some() {
1028 return Err(Error::Render(format!(
1029 "{} uses visible.mustMatch but the value could not be resolved",
1030 visibility_context(label, path)
1031 )));
1032 }
1033 return Ok(true);
1034 };
1035
1036 let json_val = val.to_json_value();
1037 if cond.hides_for_if_not_in(&json_val) {
1038 return Ok(false);
1039 }
1040 if cond.must_match.is_some() {
1041 if cond.matches_must_match(&json_val) {
1042 return Ok(false);
1043 }
1044 return Err(Error::Render(format!(
1045 "{} failed visible.mustMatch",
1046 visibility_context(label, path)
1047 )));
1048 }
1049 Ok(true)
1050 }
1051 }
1052}
1053
1054pub(crate) fn selector_from_argument_value(val: &ArgumentValue) -> Option<[u8; 4]> {
1055 match val {
1056 ArgumentValue::FixedBytes(bytes) | ArgumentValue::Bytes(bytes) if bytes.len() >= 4 => {
1057 let mut selector = [0u8; 4];
1058 selector.copy_from_slice(&bytes[..4]);
1059 Some(selector)
1060 }
1061 ArgumentValue::Uint(bytes) | ArgumentValue::Int(bytes) if bytes.len() >= 4 => {
1062 let mut selector = [0u8; 4];
1063 selector.copy_from_slice(&bytes[bytes.len() - 4..]);
1064 Some(selector)
1065 }
1066 _ => None,
1067 }
1068}
1069
1070pub(crate) fn chain_id_from_argument_value(val: &ArgumentValue) -> Option<u64> {
1071 match val {
1072 ArgumentValue::Uint(bytes) => {
1073 let n = BigUint::from_bytes_be(bytes);
1074 u64::try_from(n).ok()
1075 }
1076 _ => None,
1077 }
1078}
1079
1080pub(crate) fn uint_bytes_from_argument_value(val: &ArgumentValue) -> Option<Vec<u8>> {
1081 match val {
1082 ArgumentValue::Uint(bytes) | ArgumentValue::Int(bytes) => Some(bytes.clone()),
1083 _ => None,
1084 }
1085}
1086
1087pub(crate) fn uint_bytes_from_biguint(value: &BigUint, param_name: &str) -> Result<Vec<u8>, Error> {
1088 let bytes = value.to_bytes_be();
1089 if bytes.len() > 32 {
1090 return Err(Error::Descriptor(format!(
1091 "nested calldata param '{}' exceeds 32 bytes",
1092 param_name
1093 )));
1094 }
1095 let mut padded = vec![0u8; 32usize.saturating_sub(bytes.len())];
1096 padded.extend_from_slice(&bytes);
1097 Ok(padded)
1098}
1099
1100pub(crate) fn parse_nested_amount_literal(
1101 value: &UintLiteral,
1102 param_name: &str,
1103) -> Result<Vec<u8>, Error> {
1104 let biguint = value.to_biguint().ok_or_else(|| {
1105 Error::Descriptor(format!(
1106 "invalid nested calldata param '{}': expected a non-negative integer",
1107 param_name
1108 ))
1109 })?;
1110 uint_bytes_from_biguint(&biguint, param_name)
1111}
1112
1113pub(crate) fn parse_nested_selector_param(value: &str, param_name: &str) -> Result<[u8; 4], Error> {
1114 let hex_str = value
1115 .strip_prefix("0x")
1116 .or_else(|| value.strip_prefix("0X"))
1117 .unwrap_or(value);
1118 let bytes = hex::decode(hex_str).map_err(|_| {
1119 Error::Descriptor(format!(
1120 "invalid nested calldata param '{}': expected 4-byte hex selector",
1121 param_name
1122 ))
1123 })?;
1124 if bytes.len() != 4 {
1125 return Err(Error::Descriptor(format!(
1126 "invalid nested calldata param '{}': expected 4-byte hex selector",
1127 param_name
1128 )));
1129 }
1130 let mut selector = [0u8; 4];
1131 selector.copy_from_slice(&bytes);
1132 Ok(selector)
1133}
1134
1135pub(crate) fn parse_nested_address_param(value: &str, param_name: &str) -> Result<String, Error> {
1136 let hex_str = value
1137 .strip_prefix("0x")
1138 .or_else(|| value.strip_prefix("0X"))
1139 .unwrap_or(value);
1140 let bytes = hex::decode(hex_str).map_err(|_| {
1141 Error::Descriptor(format!(
1142 "invalid nested calldata param '{}': expected 20-byte hex address",
1143 param_name
1144 ))
1145 })?;
1146 let addr = address_bytes_from_raw_bytes(&bytes).ok_or_else(|| {
1147 Error::Descriptor(format!(
1148 "invalid nested calldata param '{}': expected 20-byte hex address",
1149 param_name
1150 ))
1151 })?;
1152 Ok(format!("0x{}", hex::encode(addr)))
1153}
1154
1155pub(crate) fn normalized_nested_calldata(
1156 inner_calldata: &[u8],
1157 selector_override: Option<[u8; 4]>,
1158) -> Vec<u8> {
1159 match selector_override {
1160 Some(selector) if !inner_calldata.starts_with(&selector) => {
1161 let mut normalized = selector.to_vec();
1162 normalized.extend_from_slice(inner_calldata);
1163 normalized
1164 }
1165 _ => inner_calldata.to_vec(),
1166 }
1167}
1168
1169pub(crate) fn ensure_single_nested_param_source(
1170 constant_present: bool,
1171 path_present: bool,
1172 param_name: &str,
1173) -> Result<(), Error> {
1174 if constant_present && path_present {
1175 return Err(Error::Descriptor(format!(
1176 "nested calldata param '{}' cannot specify both constant and path forms",
1177 param_name
1178 )));
1179 }
1180 Ok(())
1181}
1182
1183fn resolve_nested_callee(
1184 decoded: &DecodedArguments,
1185 params: Option<&FormatParams>,
1186) -> Result<Option<String>, Error> {
1187 let Some(params) = params else {
1188 return Ok(None);
1189 };
1190 ensure_single_nested_param_source(
1191 params.callee.is_some(),
1192 params.callee_path.is_some(),
1193 "callee",
1194 )?;
1195 if let Some(callee) = params.callee.as_deref() {
1196 return parse_nested_address_param(callee, "callee").map(Some);
1197 }
1198 Ok(params
1199 .callee_path
1200 .as_ref()
1201 .and_then(|path| resolve_path(decoded, path))
1202 .and_then(|value| address_string_from_argument_value(&value)))
1203}
1204
1205fn resolve_nested_spender(
1206 decoded: &DecodedArguments,
1207 params: Option<&FormatParams>,
1208) -> Result<Option<String>, Error> {
1209 let Some(params) = params else {
1210 return Ok(None);
1211 };
1212 ensure_single_nested_param_source(
1213 params.spender.is_some(),
1214 params.spender_path.is_some(),
1215 "spender",
1216 )?;
1217 if let Some(spender) = params.spender.as_deref() {
1218 return parse_nested_address_param(spender, "spender").map(Some);
1219 }
1220 Ok(params
1221 .spender_path
1222 .as_ref()
1223 .and_then(|path| resolve_path(decoded, path))
1224 .and_then(|value| address_string_from_argument_value(&value)))
1225}
1226
1227fn resolve_nested_amount(
1228 decoded: &DecodedArguments,
1229 params: Option<&FormatParams>,
1230) -> Result<Option<Vec<u8>>, Error> {
1231 let Some(params) = params else {
1232 return Ok(None);
1233 };
1234 ensure_single_nested_param_source(
1235 params.amount.is_some(),
1236 params.amount_path.is_some(),
1237 "amount",
1238 )?;
1239 if let Some(amount) = params.amount.as_ref() {
1240 return parse_nested_amount_literal(amount, "amount").map(Some);
1241 }
1242 Ok(params
1243 .amount_path
1244 .as_ref()
1245 .and_then(|path| resolve_path(decoded, path))
1246 .and_then(|value| uint_bytes_from_argument_value(&value)))
1247}
1248
1249fn resolve_nested_chain_id(
1250 decoded: &DecodedArguments,
1251 params: Option<&FormatParams>,
1252 default_chain_id: u64,
1253) -> Result<u64, Error> {
1254 let Some(params) = params else {
1255 return Ok(default_chain_id);
1256 };
1257 ensure_single_nested_param_source(
1258 params.chain_id.is_some(),
1259 params.chain_id_path.is_some(),
1260 "chainId",
1261 )?;
1262 if let Some(chain_id) = params.chain_id {
1263 return Ok(chain_id);
1264 }
1265 Ok(params
1266 .chain_id_path
1267 .as_ref()
1268 .and_then(|path| resolve_path(decoded, path))
1269 .and_then(|value| chain_id_from_argument_value(&value))
1270 .unwrap_or(default_chain_id))
1271}
1272
1273fn resolve_nested_selector(
1274 decoded: &DecodedArguments,
1275 params: Option<&FormatParams>,
1276) -> Result<Option<[u8; 4]>, Error> {
1277 let Some(params) = params else {
1278 return Ok(None);
1279 };
1280 ensure_single_nested_param_source(
1281 params.selector.is_some(),
1282 params.selector_path.is_some(),
1283 "selector",
1284 )?;
1285 if let Some(selector) = params.selector.as_deref() {
1286 return parse_nested_selector_param(selector, "selector").map(Some);
1287 }
1288 Ok(params
1289 .selector_path
1290 .as_ref()
1291 .and_then(|path| resolve_path(decoded, path))
1292 .and_then(|value| selector_from_argument_value(&value)))
1293}
1294
1295#[allow(clippy::too_many_arguments)]
1297async fn format_value(
1298 ctx: &RenderContext<'_>,
1299 value: &Option<ArgumentValue>,
1300 format: Option<&FieldFormat>,
1301 params: Option<&FormatParams>,
1302 path: &str,
1303 label: &str,
1304 separator: Option<&str>,
1305 warnings: &mut RenderDiagnostics,
1306) -> Result<String, Error> {
1307 let Some(val) = value else {
1308 warnings.push(render_warning(
1309 RenderDiagnosticKind::ValueUnresolved,
1310 format!("could not resolve path: {} for field '{}'", path, label),
1311 ));
1312 return Ok("<unresolved>".to_string());
1313 };
1314
1315 if let Some(params) = params {
1317 if let Some(ref enc) = params.encryption {
1318 if let Some(ref fallback) = enc.fallback_label {
1319 return Ok(fallback.clone());
1320 }
1321 }
1322 }
1323
1324 if let Some(params) = params {
1326 if let Some(ref map_ref) = params.map_reference {
1327 if let Some(mapped) = resolve_map(ctx, map_ref, val) {
1328 return Ok(mapped);
1329 }
1330 }
1331 }
1332
1333 let Some(fmt) = format else {
1334 return Ok(format_raw_with_separator(val, separator));
1335 };
1336
1337 match fmt {
1338 FieldFormat::TokenAmount => {
1339 format_token_amount(ctx, val, params, label, path, warnings).await
1340 }
1341 FieldFormat::Amount => format_amount(ctx, val, path),
1342 FieldFormat::Date => {
1343 format_date(ctx, val, params.and_then(|p| p.encoding.as_deref())).await
1344 }
1345 FieldFormat::Enum => format_enum(ctx, val, params),
1346 FieldFormat::Address => Ok(format_address(val)),
1347 FieldFormat::AddressName => format_address_name(ctx, val, params).await,
1348 FieldFormat::Number => Ok(format_number(val)),
1349 FieldFormat::Raw => Ok(format_raw_with_separator(val, separator)),
1350 FieldFormat::TokenTicker => format_token_ticker(ctx, val, params, warnings).await,
1351 FieldFormat::ChainId => format_chain_id(val),
1352 FieldFormat::Duration => Ok(format_duration(val)?),
1353 FieldFormat::Unit => Ok(format_unit(val, params)?),
1354 FieldFormat::Calldata => {
1355 warnings.push(render_warning(
1357 RenderDiagnosticKind::GenericRenderWarning,
1358 format!(
1359 "calldata format should be handled by render_calldata_field for field '{}' (path: {})",
1360 label, path
1361 ),
1362 ));
1363 Ok(format_raw(val))
1364 }
1365 FieldFormat::NftName => format_nft_name(ctx, val, params, label, path, warnings).await,
1366 FieldFormat::InteroperableAddressName => {
1367 warnings.push(render_warning(
1369 RenderDiagnosticKind::InteroperableAddressNameFallback,
1370 "interoperableAddressName: falling back to addressName",
1371 ));
1372 format_address_name(ctx, val, params).await
1373 }
1374 }
1375}
1376
1377async fn render_calldata_field(
1379 ctx: &RenderContext<'_>,
1380 val: &Option<ArgumentValue>,
1381 params: Option<&FormatParams>,
1382 label: &str,
1383 warnings: &mut RenderDiagnostics,
1384 nested_fallback: &mut bool,
1385) -> Result<DisplayEntry, Error> {
1386 let inner_calldata = match val {
1388 Some(ArgumentValue::Bytes(bytes)) => bytes,
1389 _ => {
1390 let raw = val
1391 .as_ref()
1392 .map(format_raw)
1393 .unwrap_or_else(|| "<unresolved>".to_string());
1394 warnings.push(render_warning(
1395 RenderDiagnosticKind::NestedCalldataInvalidType,
1396 "calldata field is not bytes",
1397 ));
1398 *nested_fallback = true;
1399 return Ok(DisplayEntry::Nested {
1400 label: label.to_string(),
1401 intent: "Unknown".to_string(),
1402 entries: vec![DisplayEntry::Item(DisplayItem {
1403 label: "Raw data".to_string(),
1404 value: raw,
1405 })],
1406 });
1407 }
1408 };
1409
1410 if ctx.depth >= MAX_CALLDATA_DEPTH {
1412 warnings.push(render_warning(
1413 RenderDiagnosticKind::NestedCalldataDegraded,
1414 format!(
1415 "nested calldata depth limit ({}) reached",
1416 MAX_CALLDATA_DEPTH
1417 ),
1418 ));
1419 *nested_fallback = true;
1420 return Ok(DisplayEntry::Nested {
1421 label: label.to_string(),
1422 intent: "Unknown".to_string(),
1423 entries: vec![DisplayEntry::Item(DisplayItem {
1424 label: "Raw data".to_string(),
1425 value: format!("0x{}", hex::encode(inner_calldata)),
1426 })],
1427 });
1428 }
1429
1430 let callee = match resolve_nested_callee(ctx.decoded, params)? {
1431 Some(addr) => addr,
1432 None => {
1433 warnings.push(render_warning(
1435 RenderDiagnosticKind::NestedCalldataDegraded,
1436 "nested calldata callee could not be resolved",
1437 ));
1438 *nested_fallback = true;
1439 return Ok(build_raw_nested(label, inner_calldata));
1440 }
1441 };
1442
1443 let amount_bytes = resolve_nested_amount(ctx.decoded, params)?;
1444 let spender_addr = resolve_nested_spender(ctx.decoded, params)?;
1445 let inner_chain_id = resolve_nested_chain_id(ctx.decoded, params, ctx.chain_id)?;
1446 let selector_override = resolve_nested_selector(ctx.decoded, params)?;
1447 let normalized_calldata = normalized_nested_calldata(inner_calldata, selector_override);
1448
1449 if normalized_calldata.len() < 4 {
1450 warnings.push(render_warning(
1451 RenderDiagnosticKind::NestedCalldataDegraded,
1452 "inner calldata too short",
1453 ));
1454 *nested_fallback = true;
1455 return Ok(DisplayEntry::Nested {
1456 label: label.to_string(),
1457 intent: "Unknown".to_string(),
1458 entries: vec![DisplayEntry::Item(DisplayItem {
1459 label: "Raw data".to_string(),
1460 value: format!("0x{}", hex::encode(inner_calldata)),
1461 })],
1462 });
1463 }
1464
1465 let inner_descriptor = ctx.descriptors.iter().find(|rd| {
1467 rd.descriptor.context.deployments().iter().any(|dep| {
1468 dep.chain_id == inner_chain_id && dep.address.to_lowercase() == callee.to_lowercase()
1469 })
1470 });
1471
1472 let inner_descriptor = match inner_descriptor {
1473 Some(rd) => &rd.descriptor,
1474 None => {
1475 warnings.push(render_warning(
1476 RenderDiagnosticKind::NestedDescriptorNotFound,
1477 "No matching descriptor for inner call",
1478 ));
1479 *nested_fallback = true;
1480 return Ok(build_raw_nested(label, inner_calldata));
1481 }
1482 };
1483
1484 let mut actual_selector = [0u8; 4];
1485 actual_selector.copy_from_slice(&normalized_calldata[..4]);
1486
1487 let (sig, _format_key) =
1489 match crate::find_matching_signature(inner_descriptor, &actual_selector) {
1490 Ok(result) => result,
1491 Err(_) => {
1492 warnings.push(render_warning(
1493 RenderDiagnosticKind::NestedDescriptorNotFound,
1494 "No matching descriptor for inner call",
1495 ));
1496 *nested_fallback = true;
1497 return Ok(build_raw_nested(label, inner_calldata));
1498 }
1499 };
1500
1501 let mut decoded = match crate::decoder::decode_calldata(&sig, &normalized_calldata) {
1503 Ok(d) => d,
1504 Err(_) => {
1505 warnings.push(render_warning(
1506 RenderDiagnosticKind::NestedCalldataDegraded,
1507 "inner calldata could not be decoded",
1508 ));
1509 *nested_fallback = true;
1510 return Ok(build_raw_nested(label, inner_calldata));
1511 }
1512 };
1513
1514 crate::inject_container_values(
1516 &mut decoded,
1517 inner_chain_id,
1518 &callee,
1519 amount_bytes.as_deref(),
1520 spender_addr.as_deref(),
1521 );
1522
1523 let inner_format =
1525 match find_format(inner_descriptor, &decoded.function_name, &decoded.selector) {
1526 Ok(f) => f,
1527 Err(_) => {
1528 warnings.push(render_warning(
1529 RenderDiagnosticKind::NestedDescriptorNotFound,
1530 "No matching descriptor for inner call",
1531 ));
1532 *nested_fallback = true;
1533 return Ok(build_raw_nested(label, inner_calldata));
1534 }
1535 };
1536
1537 let inner_ctx = RenderContext {
1538 descriptor: inner_descriptor,
1539 decoded: &decoded,
1540 chain_id: inner_chain_id,
1541 data_provider: ctx.data_provider,
1542 descriptors: ctx.descriptors,
1543 depth: ctx.depth + 1,
1544 };
1545
1546 let mut inner_warnings = Vec::new();
1547 let inner_entries = render_fields(
1548 &inner_ctx,
1549 &inner_format.fields,
1550 &mut inner_warnings,
1551 nested_fallback,
1552 )
1553 .await?;
1554
1555 warnings.extend(inner_warnings);
1556
1557 let intent = inner_format
1558 .intent
1559 .as_ref()
1560 .map(crate::types::display::intent_as_string)
1561 .unwrap_or_else(|| decoded.function_name.clone());
1562
1563 Ok(DisplayEntry::Nested {
1564 label: label.to_string(),
1565 intent,
1566 entries: inner_entries,
1567 })
1568}
1569
1570pub(crate) fn build_raw_nested(label: &str, calldata: &[u8]) -> DisplayEntry {
1572 let selector = if calldata.len() >= 4 {
1573 format!("0x{}", hex::encode(&calldata[..4]))
1574 } else {
1575 format!("0x{}", hex::encode(calldata))
1576 };
1577
1578 let data = if calldata.len() > 4 {
1579 &calldata[4..]
1580 } else {
1581 &[]
1582 };
1583
1584 let mut entries = Vec::new();
1585 for (i, chunk) in data.chunks(32).enumerate() {
1586 entries.push(DisplayEntry::Item(DisplayItem {
1587 label: format!("Param {}", i),
1588 value: format!("0x{}", hex::encode(chunk)),
1589 }));
1590 }
1591
1592 DisplayEntry::Nested {
1593 label: label.to_string(),
1594 intent: format!("Unknown function {}", selector),
1595 entries,
1596 }
1597}
1598
1599fn find_current_format<'a>(ctx: &RenderContext<'a>) -> Option<&'a DisplayFormat> {
1601 let selector_hex = hex::encode(ctx.decoded.selector);
1602 for (key, format) in &ctx.descriptor.display.formats {
1603 if key == &ctx.decoded.function_name {
1604 return Some(format);
1605 }
1606 if key.contains('(') {
1607 if let Ok(parsed) = crate::decoder::parse_signature(key) {
1608 if hex::encode(parsed.selector) == selector_hex {
1609 return Some(format);
1610 }
1611 }
1612 }
1613 }
1614 None
1615}
1616
1617fn format_raw_with_separator(val: &ArgumentValue, separator: Option<&str>) -> String {
1619 match val {
1620 ArgumentValue::Array(items) => {
1621 let sep = separator.unwrap_or(", ");
1622 let rendered: Vec<String> = items.iter().map(format_raw).collect();
1623 if separator.is_some() {
1624 rendered.join(sep)
1626 } else {
1627 format!("[{}]", rendered.join(sep))
1628 }
1629 }
1630 _ => format_raw(val),
1631 }
1632}
1633
1634fn format_raw(val: &ArgumentValue) -> String {
1635 match val {
1636 ArgumentValue::Address(addr) => format!("0x{}", hex::encode(addr)),
1637 ArgumentValue::Uint(bytes) | ArgumentValue::Int(bytes) => {
1638 let n = BigUint::from_bytes_be(bytes);
1639 n.to_string()
1640 }
1641 ArgumentValue::Bool(b) => b.to_string(),
1642 ArgumentValue::Bytes(b) | ArgumentValue::FixedBytes(b) => {
1643 format!("0x{}", hex::encode(b))
1644 }
1645 ArgumentValue::String(s) => s.clone(),
1646 ArgumentValue::Array(items) => {
1647 let rendered: Vec<String> = items.iter().map(format_raw).collect();
1648 format!("[{}]", rendered.join(", "))
1649 }
1650 ArgumentValue::Tuple(items) => {
1651 let rendered: Vec<String> = items.iter().map(|(_, v)| format_raw(v)).collect();
1652 format!("({})", rendered.join(", "))
1653 }
1654 }
1655}
1656
1657pub(crate) fn address_bytes_from_raw_bytes(bytes: &[u8]) -> Option<[u8; 20]> {
1658 let addr_bytes = match bytes.len() {
1659 20 => bytes,
1660 32 => &bytes[12..32],
1661 _ => return None,
1662 };
1663 let mut addr = [0u8; 20];
1664 addr.copy_from_slice(addr_bytes);
1665 Some(addr)
1666}
1667
1668pub(crate) fn address_bytes_from_argument_value(val: &ArgumentValue) -> Option<[u8; 20]> {
1669 match val {
1670 ArgumentValue::Address(addr) => Some(*addr),
1671 ArgumentValue::Uint(bytes)
1672 | ArgumentValue::Int(bytes)
1673 | ArgumentValue::Bytes(bytes)
1674 | ArgumentValue::FixedBytes(bytes) => address_bytes_from_raw_bytes(bytes),
1675 _ => None,
1676 }
1677}
1678
1679pub(crate) fn address_string_from_argument_value(val: &ArgumentValue) -> Option<String> {
1680 address_bytes_from_argument_value(val).map(|addr| format!("0x{}", hex::encode(addr)))
1681}
1682
1683fn format_address(val: &ArgumentValue) -> String {
1684 address_bytes_from_argument_value(val)
1685 .map(|addr| eip55_checksum(&addr))
1686 .unwrap_or_else(|| format_raw(val))
1687}
1688
1689async fn format_address_name(
1696 ctx: &RenderContext<'_>,
1697 val: &ArgumentValue,
1698 params: Option<&FormatParams>,
1699) -> Result<String, Error> {
1700 let Some(addr) = address_bytes_from_argument_value(val) else {
1701 return Ok(format_raw(val));
1702 };
1703
1704 let hex_addr = format!("0x{}", hex::encode(addr));
1705
1706 if let Some(params) = params {
1708 if let Some(ref sender) = params.sender_address {
1709 let sender_addrs = match sender {
1710 SenderAddress::Single(s) => vec![s.as_str()],
1711 SenderAddress::Multiple(v) => v.iter().map(|s| s.as_str()).collect(),
1712 };
1713 for sender_ref in &sender_addrs {
1714 let resolved_addr = if sender_ref.starts_with("@.") || sender_ref.starts_with('#') {
1716 resolve_path(ctx.decoded, sender_ref)
1717 .and_then(|v| address_string_from_argument_value(&v))
1718 } else {
1719 Some(sender_ref.to_string())
1720 };
1721 if let Some(resolved) = resolved_addr {
1722 if resolved.to_lowercase() == hex_addr.to_lowercase() {
1723 return Ok("Sender".to_string());
1724 }
1725 }
1726 }
1727 }
1728 }
1729
1730 let sources = params.and_then(|p| p.sources.as_ref());
1732 let local_allowed = sources
1733 .map(|s| s.iter().any(|src| src == "local"))
1734 .unwrap_or(true);
1735 let ens_allowed = sources
1736 .map(|s| s.iter().any(|src| src == "ens"))
1737 .unwrap_or(true);
1738
1739 if local_allowed {
1741 if let Some(name) = ctx
1742 .data_provider
1743 .resolve_local_name(
1744 &hex_addr,
1745 ctx.chain_id,
1746 params.and_then(|p| p.types.as_deref()),
1747 )
1748 .await
1749 {
1750 return Ok(name);
1751 }
1752 }
1753
1754 if ens_allowed {
1756 if let Some(name) = ctx
1757 .data_provider
1758 .resolve_ens_name(
1759 &hex_addr,
1760 ctx.chain_id,
1761 params.and_then(|p| p.types.as_deref()),
1762 )
1763 .await
1764 {
1765 return Ok(name);
1766 }
1767 }
1768
1769 Ok(eip55_checksum(&addr))
1771}
1772
1773fn eip55_checksum(addr: &[u8; 20]) -> String {
1775 use tiny_keccak::{Hasher, Keccak};
1776
1777 let hex_addr = hex::encode(addr);
1778 let mut hasher = Keccak::v256();
1779 hasher.update(hex_addr.as_bytes());
1780 let mut hash = [0u8; 32];
1781 hasher.finalize(&mut hash);
1782
1783 let mut result = String::with_capacity(42);
1784 result.push_str("0x");
1785 for (i, c) in hex_addr.chars().enumerate() {
1786 let hash_nibble = if i % 2 == 0 {
1787 (hash[i / 2] >> 4) & 0x0f
1788 } else {
1789 hash[i / 2] & 0x0f
1790 };
1791 if hash_nibble >= 8 {
1792 result.push(c.to_ascii_uppercase());
1793 } else {
1794 result.push(c);
1795 }
1796 }
1797 result
1798}
1799
1800fn format_number(val: &ArgumentValue) -> String {
1801 match val {
1802 ArgumentValue::Int(bytes) => int_to_bigint(bytes).to_string(),
1803 _ => coerce_unsigned_decimal_string_from_argument_value(val)
1804 .unwrap_or_else(|| format_raw(val)),
1805 }
1806}
1807
1808fn unsigned_biguint_from_argument_value_including_int(val: &ArgumentValue) -> Option<BigUint> {
1809 match val {
1810 ArgumentValue::Int(bytes) => Some(BigUint::from_bytes_be(bytes)),
1811 _ => None,
1812 }
1813 .or_else(|| coerce_unsigned_biguint_from_argument_value(val))
1814}
1815
1816fn unsigned_decimal_string_from_argument_value_including_int(
1817 val: &ArgumentValue,
1818) -> Option<String> {
1819 unsigned_biguint_from_argument_value_including_int(val).map(|n| n.to_string())
1820}
1821
1822fn unsigned_biguint_from_argument_value_for_amount(val: &ArgumentValue) -> Option<BigUint> {
1823 match val {
1824 ArgumentValue::Int(bytes) => Some(BigUint::from_bytes_be(bytes)),
1825 _ => coerce_unsigned_biguint_from_argument_value(val),
1826 }
1827}
1828
1829fn int_to_bigint(bytes: &[u8]) -> BigInt {
1831 if bytes.is_empty() {
1832 return BigInt::from(0);
1833 }
1834 if bytes[0] & 0x80 != 0 {
1836 let inverted: Vec<u8> = bytes.iter().map(|b| !b).collect();
1838 let magnitude = BigUint::from_bytes_be(&inverted) + 1u64;
1839 BigInt::from_biguint(Sign::Minus, magnitude)
1840 } else {
1841 BigInt::from_biguint(Sign::Plus, BigUint::from_bytes_be(bytes))
1842 }
1843}
1844
1845async fn format_token_amount(
1846 ctx: &RenderContext<'_>,
1847 val: &ArgumentValue,
1848 params: Option<&FormatParams>,
1849 label: &str,
1850 path: &str,
1851 warnings: &mut RenderDiagnostics,
1852) -> Result<String, Error> {
1853 let Some(raw_amount) = unsigned_biguint_from_argument_value_including_int(val) else {
1854 return Ok(format_raw(val));
1855 };
1856
1857 let lookup_chain_id = resolve_chain_id(ctx, params);
1859
1860 let token_meta = if let Some(params) = params {
1862 if let Some(ref token_path) = params.token_path {
1863 let token_addr = resolve_path(ctx.decoded, token_path);
1865 let addr_hex = token_addr
1866 .as_ref()
1867 .and_then(address_string_from_argument_value);
1868 if let Some(ref addr_hex) = addr_hex {
1869 if let Some(ref native) = params.native_currency_address {
1871 if native.matches(addr_hex, &ctx.descriptor.metadata.constants) {
1872 Some(native_token_meta(lookup_chain_id))
1873 } else {
1874 ctx.data_provider
1875 .resolve_token(lookup_chain_id, addr_hex)
1876 .await
1877 }
1878 } else {
1879 ctx.data_provider
1880 .resolve_token(lookup_chain_id, addr_hex)
1881 .await
1882 }
1883 } else {
1884 None
1885 }
1886 } else if let Some(ref token_ref) = params.token {
1887 let addr = resolve_metadata_constant_str(ctx.descriptor, token_ref);
1889 if let Some(ref native) = params.native_currency_address {
1890 if native.matches(&addr, &ctx.descriptor.metadata.constants) {
1891 Some(native_token_meta(lookup_chain_id))
1892 } else {
1893 ctx.data_provider
1894 .resolve_token(lookup_chain_id, &addr)
1895 .await
1896 }
1897 } else {
1898 ctx.data_provider
1899 .resolve_token(lookup_chain_id, &addr)
1900 .await
1901 }
1902 } else {
1903 None
1904 }
1905 } else {
1906 None
1907 };
1908
1909 if token_meta.is_none() {
1910 warnings.push(render_warning(
1911 RenderDiagnosticKind::TokenMetadataNotFound,
1912 format!(
1913 "token metadata not found for field '{}' (path: {})",
1914 label, path
1915 ),
1916 ));
1917 }
1918
1919 Ok(format_token_amount_output(
1920 ctx.descriptor,
1921 &raw_amount,
1922 params,
1923 token_meta.as_ref(),
1924 ))
1925}
1926
1927async fn format_token_ticker(
1928 ctx: &RenderContext<'_>,
1929 val: &ArgumentValue,
1930 params: Option<&FormatParams>,
1931 warnings: &mut RenderDiagnostics,
1932) -> Result<String, Error> {
1933 let lookup_chain_id = resolve_chain_id(ctx, params);
1934
1935 if let Some(addr_hex) = address_string_from_argument_value(val) {
1936 if let Some(meta) = ctx
1937 .data_provider
1938 .resolve_token(lookup_chain_id, &addr_hex)
1939 .await
1940 {
1941 return Ok(meta.symbol);
1942 }
1943 }
1944
1945 warnings.push(render_warning(
1946 RenderDiagnosticKind::TokenTickerNotFound,
1947 "token ticker not found",
1948 ));
1949 Ok(format_raw(val))
1950}
1951
1952fn format_chain_id(val: &ArgumentValue) -> Result<String, Error> {
1953 if let ArgumentValue::Uint(bytes) = val {
1954 let n = BigUint::from_bytes_be(bytes);
1955 let chain_id: u64 = n.try_into().unwrap_or(0);
1956 Ok(chain_name(chain_id))
1957 } else {
1958 Ok(format_raw(val))
1959 }
1960}
1961
1962fn resolve_chain_id(ctx: &RenderContext<'_>, params: Option<&FormatParams>) -> u64 {
1964 if let Some(params) = params {
1965 if let Some(cid) = params.chain_id {
1967 return cid;
1968 }
1969 if let Some(ref path) = params.chain_id_path {
1971 if let Some(ArgumentValue::Uint(bytes)) = resolve_path(ctx.decoded, path) {
1972 let n = BigUint::from_bytes_be(&bytes);
1973 if let Ok(cid) = u64::try_from(n) {
1974 return cid;
1975 }
1976 }
1977 }
1978 }
1979 ctx.chain_id
1980}
1981
1982fn format_amount(
1983 ctx: &RenderContext<'_>,
1984 val: &ArgumentValue,
1985 path: &str,
1986) -> Result<String, Error> {
1987 let Some(n) = unsigned_biguint_from_argument_value_for_amount(val) else {
1988 return Ok(format_raw(val));
1989 };
1990
1991 if path.starts_with("@.value") {
1992 let meta = native_token_meta(ctx.chain_id);
1993 let formatted = format_with_decimals(&n, meta.decimals);
1994 Ok(format!("{} {}", formatted, meta.symbol))
1995 } else {
1996 Ok(n.to_string())
1997 }
1998}
1999
2000async fn format_date(
2001 ctx: &RenderContext<'_>,
2002 val: &ArgumentValue,
2003 encoding: Option<&str>,
2004) -> Result<String, Error> {
2005 match val {
2006 ArgumentValue::Uint(bytes) => {
2007 let n = BigUint::from_bytes_be(bytes);
2008 if encoding == Some("blockheight") {
2009 let block_number = u64::try_from(&n).map_err(|_| {
2010 Error::Render(format!("blockheight {} does not fit into u64", n))
2011 })?;
2012 return format_blockheight_timestamp(ctx.data_provider, ctx.chain_id, block_number)
2013 .await;
2014 }
2015
2016 let timestamp = i64::try_from(&n)
2017 .map_err(|_| Error::Render(format!("timestamp {} does not fit into i64", n)))?;
2018 format_timestamp(timestamp)
2019 }
2020 _ => Ok(format_raw(val)),
2021 }
2022}
2023
2024fn format_enum(
2025 ctx: &RenderContext<'_>,
2026 val: &ArgumentValue,
2027 params: Option<&FormatParams>,
2028) -> Result<String, Error> {
2029 let raw = unsigned_decimal_string_from_argument_value_including_int(val)
2030 .unwrap_or_else(|| format_raw(val));
2031
2032 if let Some(params) = params {
2033 if let Some(ref enum_path) = params.enum_path {
2035 if let Some(enum_def) = ctx.descriptor.metadata.enums.get(enum_path) {
2036 if let Some(label) = enum_def.get(&raw) {
2037 return Ok(label.clone());
2038 }
2039 }
2040 }
2041 if let Some(ref ref_path) = params.ref_path {
2043 if let Some(enum_name) = ref_path.strip_prefix("$.metadata.enums.") {
2044 if let Some(enum_def) = ctx.descriptor.metadata.enums.get(enum_name) {
2045 if let Some(label) = enum_def.get(&raw) {
2046 return Ok(label.clone());
2047 }
2048 }
2049 }
2050 }
2051 }
2052
2053 Ok(raw)
2054}
2055
2056fn resolve_map(ctx: &RenderContext<'_>, map_ref: &str, val: &ArgumentValue) -> Option<String> {
2060 let key = if let Some(ref key_path) = ctx.descriptor.metadata.maps.get(map_ref)?.key_path {
2061 resolve_path(ctx.decoded, key_path).map(|v| format_raw(&v))?
2062 } else {
2063 format_raw(val)
2064 };
2065 lookup_map_entry(ctx.descriptor, map_ref, &key)
2066}
2067
2068async fn format_nft_name(
2070 ctx: &RenderContext<'_>,
2071 val: &ArgumentValue,
2072 params: Option<&FormatParams>,
2073 label: &str,
2074 path: &str,
2075 warnings: &mut RenderDiagnostics,
2076) -> Result<String, Error> {
2077 let token_id = match val {
2079 ArgumentValue::Uint(bytes) | ArgumentValue::Int(bytes) => {
2080 BigUint::from_bytes_be(bytes).to_string()
2081 }
2082 _ => return Ok(format_raw(val)),
2083 };
2084
2085 let collection_addr = params.and_then(|p| {
2087 if let Some(ref cpath) = p.collection_path {
2089 let resolved = resolve_path(ctx.decoded, cpath);
2090 if let Some(ArgumentValue::Address(addr)) = resolved {
2091 return Some(format!("0x{}", hex::encode(addr)));
2092 }
2093 }
2094 p.collection.clone()
2096 });
2097
2098 let Some(collection_addr) = collection_addr else {
2099 warnings.push(render_warning(
2100 RenderDiagnosticKind::NftCollectionAddressMissing,
2101 format!(
2102 "no collection address for nftName field '{}' (path: {})",
2103 label, path
2104 ),
2105 ));
2106 return Ok(token_id);
2107 };
2108
2109 if let Some(name) = ctx
2111 .data_provider
2112 .resolve_nft_collection_name(&collection_addr, ctx.chain_id)
2113 .await
2114 {
2115 Ok(format!("{} #{}", name, token_id))
2116 } else {
2117 warnings.push(render_warning(
2118 RenderDiagnosticKind::NftCollectionNameNotFound,
2119 format!(
2120 "NFT collection not found for '{}' (address: {})",
2121 label, collection_addr
2122 ),
2123 ));
2124 Ok(format!("#{}", token_id))
2125 }
2126}
2127
2128async fn interpolate_intent(
2133 template: &str,
2134 ctx: &RenderContext<'_>,
2135 fields: &[DisplayField],
2136 excluded: &[String],
2137) -> Result<String, Error> {
2138 const OPEN_SENTINEL: &str = "\x00OPEN_BRACE\x00";
2140 const CLOSE_SENTINEL: &str = "\x00CLOSE_BRACE\x00";
2141 let mut result = template
2142 .replace("{{", OPEN_SENTINEL)
2143 .replace("}}", CLOSE_SENTINEL);
2144
2145 while let Some(start) = result.find("${") {
2147 let end = match result[start..].find('}') {
2148 Some(e) => start + e,
2149 None => break,
2150 };
2151 let path = result[start + 2..end].to_string();
2152 let replacement =
2153 resolve_and_format_for_interpolation(ctx, fields, excluded, &path).await?;
2154 result.replace_range(start..=end, &replacement);
2155 }
2156
2157 let mut pos = 0;
2159 while pos < result.len() {
2160 if let Some(rel_start) = result[pos..].find('{') {
2161 let start = pos + rel_start;
2162 if start > 0 && result.as_bytes()[start - 1] == b'$' {
2164 pos = start + 1;
2165 continue;
2166 }
2167 let end = match result[start..].find('}') {
2168 Some(e) => start + e,
2169 None => break,
2170 };
2171 let path = result[start + 1..end].to_string();
2172 let replacement =
2173 resolve_and_format_for_interpolation(ctx, fields, excluded, &path).await?;
2174 result.replace_range(start..=end, &replacement);
2175 pos = start + replacement.len();
2176 } else {
2177 break;
2178 }
2179 }
2180
2181 let result = result
2183 .replace(OPEN_SENTINEL, "{")
2184 .replace(CLOSE_SENTINEL, "}");
2185
2186 Ok(result)
2187}
2188
2189fn format_duration(val: &ArgumentValue) -> Result<String, Error> {
2191 let secs = match val {
2192 ArgumentValue::Uint(bytes) | ArgumentValue::Int(bytes) => {
2193 let n = BigUint::from_bytes_be(bytes);
2194 u64::try_from(&n)
2195 .map_err(|_| Error::Render(format!("duration {} does not fit into u64", n)))?
2196 }
2197 _ => return Ok(format_raw(val)),
2198 };
2199
2200 Ok(format_duration_seconds(secs))
2201}
2202
2203fn format_unit(val: &ArgumentValue, params: Option<&FormatParams>) -> Result<String, Error> {
2205 let Some(raw_val) = unsigned_biguint_from_argument_value_including_int(val) else {
2206 return Ok(format_raw(val));
2207 };
2208
2209 Ok(format_unit_biguint(&raw_val, params))
2210}
2211
2212async fn resolve_and_format_for_interpolation(
2213 ctx: &RenderContext<'_>,
2214 fields: &[DisplayField],
2215 excluded: &[String],
2216 path: &str,
2217) -> Result<String, Error> {
2218 let field = resolve_interpolation_field_spec(fields, excluded, path)?;
2219
2220 let value = resolve_path(ctx.decoded, field.path).ok_or_else(|| {
2221 Error::Descriptor(format!(
2222 "interpolatedIntent path '{}' could not be resolved from calldata",
2223 path
2224 ))
2225 })?;
2226
2227 let mut warnings = RenderDiagnostics::new();
2228 format_value(
2229 ctx,
2230 &Some(value),
2231 field.format,
2232 field.params,
2233 field.path,
2234 field.label,
2235 field.separator,
2236 &mut warnings,
2237 )
2238 .await
2239}
2240
2241pub(crate) fn record_diagnostics(state: &mut RenderState, diagnostics: &[FormatDiagnostic]) {
2242 for diagnostic in diagnostics {
2243 state.push_diagnostic(diagnostic.clone());
2244 }
2245}
2246
2247#[cfg(test)]
2248mod tests {
2249 use super::*;
2250 use crate::decoder::{DecodedArgument, ParamType};
2251 use crate::path::{parse_collection_access, CollectionAccess};
2252
2253 #[test]
2254 fn test_eip55_checksum() {
2255 let addr_bytes = hex::decode("5aaeb6053f3e94c9b9a09f33669435e7ef1beaed").unwrap();
2257 let mut addr = [0u8; 20];
2258 addr.copy_from_slice(&addr_bytes);
2259 let checksummed = eip55_checksum(&addr);
2260 assert_eq!(checksummed, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
2261 }
2262
2263 #[test]
2264 fn test_byte_slice_path_resolution_supports_bytes_fixedbytes_and_uint() {
2265 let decoded = DecodedArguments {
2266 function_name: "demo".to_string(),
2267 selector: [0; 4],
2268 args: vec![
2269 DecodedArgument {
2270 index: 0,
2271 name: Some("payload".to_string()),
2272 param_type: ParamType::Bytes,
2273 value: ArgumentValue::Bytes(vec![0x11, 0x22, 0x33, 0x44]),
2274 },
2275 DecodedArgument {
2276 index: 1,
2277 name: Some("packed".to_string()),
2278 param_type: ParamType::FixedBytes(32),
2279 value: ArgumentValue::FixedBytes(
2280 hex::decode(
2281 "000000000000000000000000b21d281dedb17ae5b501f6aa8256fe38c4e45757",
2282 )
2283 .unwrap(),
2284 ),
2285 },
2286 DecodedArgument {
2287 index: 2,
2288 name: Some("packed_addr".to_string()),
2289 param_type: ParamType::Uint(256),
2290 value: ArgumentValue::Uint(
2291 hex::decode(
2292 "0000000000000000000000001111111111111111111111111111111111111111",
2293 )
2294 .unwrap(),
2295 ),
2296 },
2297 ],
2298 };
2299
2300 match resolve_path(&decoded, "payload.[1:3]") {
2301 Some(ArgumentValue::Bytes(bytes)) => assert_eq!(hex::encode(bytes), "2233"),
2302 other => panic!("unexpected payload slice: {other:?}"),
2303 }
2304 match resolve_path(&decoded, "packed.[-20:]") {
2305 Some(ArgumentValue::Bytes(bytes)) => {
2306 assert_eq!(
2307 hex::encode(bytes),
2308 "b21d281dedb17ae5b501f6aa8256fe38c4e45757"
2309 )
2310 }
2311 other => panic!("unexpected packed slice: {other:?}"),
2312 }
2313 match resolve_path(&decoded, "packed_addr.[-20:]") {
2314 Some(ArgumentValue::Bytes(bytes)) => {
2315 assert_eq!(
2316 hex::encode(bytes),
2317 "1111111111111111111111111111111111111111"
2318 )
2319 }
2320 other => panic!("unexpected packed uint slice: {other:?}"),
2321 }
2322 }
2323
2324 #[test]
2325 fn test_enum_and_address_coercions_accept_byte_like_values() {
2326 let descriptor: Descriptor = serde_json::from_str(
2327 r#"{"context":{"contract":{"deployments":[]}},"metadata":{"owner":"test","enums":{"dex":{"82":"Single swap"}},"constants":{},"maps":{}},"display":{"definitions":{},"formats":{}}}"#,
2328 )
2329 .unwrap();
2330 let decoded = DecodedArguments {
2331 function_name: "demo".to_string(),
2332 selector: [0; 4],
2333 args: vec![],
2334 };
2335 let provider = crate::provider::EmptyDataProvider;
2336 let ctx = RenderContext {
2337 descriptor: &descriptor,
2338 decoded: &decoded,
2339 chain_id: 1,
2340 data_provider: &provider,
2341 descriptors: &[],
2342 depth: 0,
2343 };
2344 let params: FormatParams =
2345 serde_json::from_value(serde_json::json!({"$ref": "$.metadata.enums.dex"})).unwrap();
2346
2347 assert_eq!(
2348 format_enum(&ctx, &ArgumentValue::Bytes(vec![0x52]), Some(¶ms)).unwrap(),
2349 "Single swap"
2350 );
2351 assert_eq!(
2352 format_address(&ArgumentValue::Bytes(
2353 hex::decode("b21d281dedb17ae5b501f6aa8256fe38c4e45757").unwrap()
2354 )),
2355 "0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757"
2356 );
2357 }
2358
2359 #[test]
2360 fn test_shared_slice_parser_supports_open_ended_bounds() {
2361 assert_eq!(
2362 parse_collection_access("[:1]", 4),
2363 Some(CollectionAccess::Slice { start: 0, end: 1 })
2364 );
2365 assert_eq!(
2366 parse_collection_access("[-2:]", 4),
2367 Some(CollectionAccess::Slice { start: 2, end: 4 })
2368 );
2369 assert_eq!(parse_collection_access("[5:2]", 4), None);
2370 }
2371
2372 #[tokio::test]
2373 async fn test_interpolate_intent() {
2374 use crate::provider::EmptyDataProvider;
2375
2376 let decoded = DecodedArguments {
2377 function_name: "transfer".to_string(),
2378 selector: [0; 4],
2379 args: vec![
2380 DecodedArgument {
2381 index: 0,
2382 name: None,
2383 param_type: ParamType::Address,
2384 value: ArgumentValue::Address([0u8; 20]),
2385 },
2386 DecodedArgument {
2387 index: 1,
2388 name: None,
2389 param_type: ParamType::Uint(256),
2390 value: ArgumentValue::Uint(vec![
2391 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2392 0, 0, 0, 0, 0, 0x03, 0xe8,
2393 ]),
2394 },
2395 ],
2396 };
2397
2398 let descriptor: Descriptor = serde_json::from_str(
2399 r#"{"context":{"contract":{"deployments":[]}},"metadata":{"owner":"test","enums":{},"constants":{},"maps":{}},"display":{"definitions":{},"formats":{}}}"#
2400 ).unwrap();
2401 let data_provider = EmptyDataProvider;
2402 let ctx = RenderContext {
2403 descriptor: &descriptor,
2404 decoded: &decoded,
2405 chain_id: 1,
2406 data_provider: &data_provider,
2407 descriptors: &[],
2408 depth: 0,
2409 };
2410
2411 let fields = vec![
2412 DisplayField::Simple {
2413 path: Some("0".to_string()),
2414 label: "To".to_string(),
2415 value: None,
2416 format: Some(FieldFormat::Address),
2417 params: None,
2418 separator: None,
2419 visible: VisibleRule::Named(VisibleLiteral::Never),
2420 },
2421 DisplayField::Simple {
2422 path: Some("1".to_string()),
2423 label: "Amount".to_string(),
2424 value: None,
2425 format: Some(FieldFormat::Number),
2426 params: None,
2427 separator: None,
2428 visible: VisibleRule::Named(VisibleLiteral::Never),
2429 },
2430 ];
2431
2432 let result = interpolate_intent("Send ${1} to ${0}", &ctx, &fields, &[])
2433 .await
2434 .unwrap();
2435 assert_eq!(
2436 result,
2437 "Send 1000 to 0x0000000000000000000000000000000000000000"
2438 );
2439 }
2440
2441 #[tokio::test]
2442 async fn test_interpolate_intent_address_name() {
2443 use crate::types::display::{DisplayField, FieldFormat};
2444
2445 struct MockLocalNameProvider;
2447 impl DataProvider for MockLocalNameProvider {
2448 fn resolve_local_name(
2449 &self,
2450 address: &str,
2451 _chain_id: u64,
2452 _types: Option<&[String]>,
2453 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>> {
2454 let addr = address.to_string();
2455 Box::pin(async move {
2456 if addr.to_lowercase() == "0xbf01daf454dce008d3e2bfd47d5e186f71477253" {
2457 Some("My Savings".to_string())
2458 } else {
2459 None
2460 }
2461 })
2462 }
2463 }
2464
2465 let mut addr_bytes = [0u8; 20];
2466 addr_bytes
2467 .copy_from_slice(&hex::decode("bf01daf454dce008d3e2bfd47d5e186f71477253").unwrap());
2468
2469 let decoded = DecodedArguments {
2470 function_name: "withdraw".to_string(),
2471 selector: [0; 4],
2472 args: vec![DecodedArgument {
2473 index: 0,
2474 name: Some("to".to_string()),
2475 param_type: ParamType::Address,
2476 value: ArgumentValue::Address(addr_bytes),
2477 }],
2478 };
2479
2480 let fields = vec![DisplayField::Simple {
2481 path: Some("to".to_string()),
2482 label: "Recipient".to_string(),
2483 value: None,
2484 format: Some(FieldFormat::AddressName),
2485 params: None,
2486 separator: None,
2487 visible: VisibleRule::Always,
2488 }];
2489
2490 let descriptor: Descriptor = serde_json::from_str(
2491 r#"{"context":{"contract":{"deployments":[]}},"metadata":{"owner":"test","enums":{},"constants":{},"maps":{}},"display":{"definitions":{},"formats":{}}}"#,
2492 )
2493 .unwrap();
2494 let data_provider = MockLocalNameProvider;
2495 let ctx = RenderContext {
2496 descriptor: &descriptor,
2497 decoded: &decoded,
2498 chain_id: 1,
2499 data_provider: &data_provider,
2500 descriptors: &[],
2501 depth: 0,
2502 };
2503
2504 let result = interpolate_intent("Withdraw to {to}", &ctx, &fields, &[])
2505 .await
2506 .unwrap();
2507 assert_eq!(result, "Withdraw to My Savings");
2508 }
2509}