1#[cfg(feature = "uniffi")]
7uniffi::setup_scaffolding!();
8
9pub mod decoder;
10pub mod eip712;
11mod eip712_domain;
12pub mod engine;
13pub mod error;
14pub mod merge;
15pub mod outcome;
16mod path;
17pub mod provider;
18mod render_shared;
19pub mod resolver;
20pub mod token;
21pub mod types;
22#[cfg(feature = "uniffi")]
23pub mod uniffi_compat;
24
25use error::Error;
26use outcome::RenderState;
27
28pub use engine::{DisplayEntry, DisplayItem, DisplayModel};
30pub use error::FormatFailure;
31pub use merge::merge_descriptors;
32pub use outcome::{
33 DescriptorResolutionOutcome, DiagnosticSeverity, FallbackReason, FormatDiagnostic,
34 FormatOutcome, ResolvedDescriptorResolution,
35};
36pub use provider::{DataProvider, EmptyDataProvider};
37#[cfg(feature = "github-registry")]
38pub use resolver::resolve_descriptors_for_typed_data;
39pub use resolver::{
40 resolve_descriptors_for_tx, DescriptorSource, ResolvedDescriptor, TypedDescriptorLookup,
41};
42pub use token::{CompositeDataProvider, TokenMeta, WellKnownTokenSource};
43pub use types::descriptor::Descriptor;
44
45pub struct TransactionContext<'a> {
47 pub chain_id: u64,
48 pub to: &'a str,
49 pub calldata: &'a [u8],
50 pub value: Option<&'a [u8]>,
51 pub from: Option<&'a str>,
52 pub implementation_address: Option<&'a str>,
56}
57
58pub async fn format_calldata(
65 descriptors: &[ResolvedDescriptor],
66 tx: &TransactionContext<'_>,
67 data_provider: &dyn DataProvider,
68) -> Result<FormatOutcome, FormatFailure> {
69 if tx.calldata.len() < 4 {
70 return Err(FormatFailure::InvalidInput {
71 message: error::DecodeError::CalldataTooShort {
72 expected: 4,
73 actual: tx.calldata.len(),
74 }
75 .to_string(),
76 retryable: false,
77 });
78 }
79
80 let match_address = tx.implementation_address.unwrap_or(tx.to);
83 let outer_idx = descriptors.iter().position(|rd| {
84 rd.descriptor.context.deployments().iter().any(|dep| {
85 dep.chain_id == tx.chain_id
86 && dep.address.to_lowercase() == match_address.to_lowercase()
87 })
88 });
89
90 let outer_idx = match outer_idx {
91 Some(idx) => idx,
92 None => {
93 if descriptors.is_empty() {
94 return Ok(fallback_outcome(
95 build_raw_fallback(tx.calldata),
96 FallbackReason::DescriptorNotFound,
97 "descriptor_not_found",
98 format!(
99 "no descriptor matched chain_id={} address={}",
100 tx.chain_id, match_address
101 ),
102 ));
103 }
104 return Err(FormatFailure::InvalidDescriptor {
105 message: format!(
106 "no outer descriptor matches chain_id={} address={}",
107 tx.chain_id, match_address
108 ),
109 retryable: false,
110 });
111 }
112 };
113
114 let outer_descriptor = &descriptors[outer_idx].descriptor;
115 let actual_selector = &tx.calldata[..4];
116
117 let (sig, _format_key) = match find_matching_signature(outer_descriptor, actual_selector) {
119 Ok(result) => result,
120 Err(_) => {
121 return Ok(fallback_outcome(
123 build_raw_fallback(tx.calldata),
124 FallbackReason::FormatNotFound,
125 "format_not_found",
126 format!(
127 "no descriptor format matched selector 0x{}",
128 hex::encode(actual_selector)
129 ),
130 ));
131 }
132 };
133
134 let mut decoded =
136 decoder::decode_calldata(&sig, tx.calldata).map_err(|err| FormatFailure::InvalidInput {
137 message: err.to_string(),
138 retryable: false,
139 })?;
140
141 inject_container_values(&mut decoded, tx.chain_id, tx.to, tx.value, tx.from);
143
144 let mut state = RenderState::default();
146 let model = engine::format_calldata(
147 outer_descriptor,
148 tx.chain_id,
149 tx.to,
150 &decoded,
151 tx.value,
152 data_provider,
153 descriptors,
154 &mut state,
155 )
156 .await
157 .map_err(FormatFailure::from)?;
158
159 Ok(state.outcome(model, None))
160}
161
162pub(crate) fn inject_container_values(
164 decoded: &mut decoder::DecodedArguments,
165 chain_id: u64,
166 to: &str,
167 value: Option<&[u8]>,
168 from: Option<&str>,
169) {
170 if let Some(val_bytes) = value {
172 let mut padded = vec![0u8; 32usize.saturating_sub(val_bytes.len())];
173 padded.extend_from_slice(val_bytes);
174 decoded.args.push(decoder::DecodedArgument {
175 index: decoded.args.len(),
176 name: Some("value".into()),
177 param_type: decoder::ParamType::Uint(256),
178 value: decoder::ArgumentValue::Uint(padded),
179 });
180 }
181
182 if let Some(addr) = parse_address_bytes(to) {
184 decoded.args.push(decoder::DecodedArgument {
185 index: decoded.args.len(),
186 name: Some("to".into()),
187 param_type: decoder::ParamType::Address,
188 value: decoder::ArgumentValue::Address(addr),
189 });
190 }
191
192 let chain_bytes = {
194 let mut buf = [0u8; 32];
195 buf[24..32].copy_from_slice(&chain_id.to_be_bytes());
196 buf.to_vec()
197 };
198 decoded.args.push(decoder::DecodedArgument {
199 index: decoded.args.len(),
200 name: Some("chainId".into()),
201 param_type: decoder::ParamType::Uint(256),
202 value: decoder::ArgumentValue::Uint(chain_bytes),
203 });
204
205 if let Some(from_addr) = from {
207 if let Some(addr) = parse_address_bytes(from_addr) {
208 decoded.args.push(decoder::DecodedArgument {
209 index: decoded.args.len(),
210 name: Some("from".into()),
211 param_type: decoder::ParamType::Address,
212 value: decoder::ArgumentValue::Address(addr),
213 });
214 }
215 }
216}
217
218pub(crate) fn parse_address_bytes(addr: &str) -> Option<[u8; 20]> {
219 let hex_str = addr
220 .strip_prefix("0x")
221 .or_else(|| addr.strip_prefix("0X"))
222 .unwrap_or(addr);
223 let bytes = hex::decode(hex_str).ok()?;
224 if bytes.len() != 20 {
225 return None;
226 }
227 let mut result = [0u8; 20];
228 result.copy_from_slice(&bytes);
229 Some(result)
230}
231
232pub(crate) fn build_raw_fallback(calldata: &[u8]) -> DisplayModel {
234 let selector = if calldata.len() >= 4 {
235 format!("0x{}", hex::encode(&calldata[..4]))
236 } else {
237 format!("0x{}", hex::encode(calldata))
238 };
239
240 let mut entries = Vec::new();
241 let data = if calldata.len() > 4 {
242 &calldata[4..]
243 } else {
244 &[]
245 };
246
247 for (i, chunk) in data.chunks(32).enumerate() {
249 entries.push(DisplayEntry::Item(DisplayItem {
250 label: format!("Param {}", i),
251 value: format!("0x{}", hex::encode(chunk)),
252 }));
253 }
254
255 DisplayModel {
256 intent: format!("Unknown function {}", selector),
257 interpolated_intent: None,
258 entries,
259 owner: None,
260 contract_name: None,
261 }
262}
263
264pub async fn format_typed_data(
270 descriptors: &[ResolvedDescriptor],
271 data: &eip712::TypedData,
272 data_provider: &dyn DataProvider,
273) -> Result<FormatOutcome, FormatFailure> {
274 if descriptors.is_empty() {
275 return Ok(fallback_outcome(
276 eip712::build_typed_raw_fallback(data),
277 FallbackReason::DescriptorNotFound,
278 "descriptor_not_found",
279 "no typed-data descriptor matched the verifying contract".to_string(),
280 ));
281 }
282
283 let chain_id = match data.domain.chain_id {
284 Some(chain_id) => chain_id,
285 None => {
286 return Ok(fallback_outcome(
287 eip712::build_typed_raw_fallback(data),
288 FallbackReason::InsufficientContext,
289 "insufficient_context",
290 "EIP-712 domain.chainId is required for descriptor-based clear signing".to_string(),
291 ));
292 }
293 };
294 let verifying_contract = match data.domain.verifying_contract.as_deref() {
295 Some(verifying_contract) => verifying_contract,
296 None => {
297 return Ok(fallback_outcome(
298 eip712::build_typed_raw_fallback(data),
299 FallbackReason::InsufficientContext,
300 "insufficient_context",
301 "EIP-712 domain.verifyingContract is required for descriptor-based clear signing"
302 .to_string(),
303 ));
304 }
305 };
306
307 let selection =
308 resolver::select_typed_outer_descriptor(descriptors, data).map_err(FormatFailure::from)?;
309
310 let selected = match selection {
311 resolver::TypedOuterSelection::Selected(selected) => selected,
312 resolver::TypedOuterSelection::NoMatch(no_match) => {
313 if no_match.domain_errors.is_empty() && no_match.format_misses.len() == 1 {
314 return Ok(fallback_outcome(
315 eip712::build_typed_raw_fallback(data),
316 FallbackReason::FormatNotFound,
317 "format_not_found",
318 format!(
319 "no descriptor format matched primaryType={} encodeType for verifying_contract={}",
320 data.primary_type, verifying_contract
321 ),
322 ));
323 }
324 let mut message = format!(
325 "no EIP-712 descriptor found for chain_id={} verifying_contract={} after domain and encodeType validation",
326 chain_id, verifying_contract
327 );
328 if !no_match.domain_errors.is_empty() {
329 message.push_str(": ");
330 message.push_str(&no_match.domain_errors.join("; "));
331 } else if !no_match.format_misses.is_empty() {
332 return Ok(fallback_outcome(
333 eip712::build_typed_raw_fallback(data),
334 FallbackReason::FormatNotFound,
335 "format_not_found",
336 "no descriptor matched the typed-data encodeType".to_string(),
337 ));
338 }
339 return Err(FormatFailure::InvalidDescriptor {
340 message,
341 retryable: false,
342 });
343 }
344 };
345
346 let mut state = RenderState::default();
347 let model = eip712::format_typed_data_with_format(
348 &selected.outer.descriptor,
349 data,
350 selected.format,
351 data_provider,
352 descriptors,
353 &mut state,
354 )
355 .await
356 .map_err(FormatFailure::from)?;
357
358 Ok(state.outcome(model, None))
359}
360
361fn fallback_outcome(
362 model: DisplayModel,
363 reason: FallbackReason,
364 code: &str,
365 message: String,
366) -> FormatOutcome {
367 let mut state = RenderState::default();
368 state.warn(code, message);
369 state.outcome(model, Some(reason))
370}
371
372pub(crate) fn find_matching_signature(
374 descriptor: &Descriptor,
375 actual_selector: &[u8],
376) -> Result<(decoder::FunctionSignature, String), Error> {
377 for key in descriptor.display.formats.keys() {
378 if key.contains('(') {
379 match decoder::parse_signature(key) {
380 Ok(sig) => {
381 if sig.selector[..] == actual_selector[..4] {
382 return Ok((sig, key.clone()));
383 }
384 }
385 Err(_) => continue,
386 }
387 }
388 }
389
390 Err(Error::Render(format!(
391 "no matching format key for selector 0x{}",
392 hex::encode(&actual_selector[..4])
393 )))
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::provider::EmptyDataProvider;
400 use crate::token::StaticTokenSource;
401
402 fn wrap_rd(descriptor: Descriptor, chain_id: u64, address: &str) -> Vec<ResolvedDescriptor> {
403 vec![ResolvedDescriptor {
404 descriptor,
405 chain_id,
406 address: address.to_lowercase(),
407 }]
408 }
409
410 fn test_descriptor_json() -> &'static str {
411 r#"{
412 "context": {
413 "contract": {
414 "deployments": [
415 { "chainId": 1, "address": "0xdac17f958d2ee523a2206206994597c13d831ec7" }
416 ]
417 }
418 },
419 "metadata": {
420 "owner": "test",
421 "contractName": "Tether USD",
422 "enums": {},
423 "constants": {},
424 "addressBook": {},
425 "maps": {}
426 },
427 "display": {
428 "definitions": {},
429 "formats": {
430 "transfer(address,uint256)": {
431 "intent": "Transfer tokens",
432 "fields": [
433 {
434 "path": "@.0",
435 "label": "To",
436 "format": "address"
437 },
438 {
439 "path": "@.1",
440 "label": "Amount",
441 "format": "number"
442 }
443 ]
444 }
445 }
446 }
447 }"#
448 }
449
450 #[tokio::test]
451 async fn test_full_calldata_pipeline() {
452 let descriptor = Descriptor::from_json(test_descriptor_json()).unwrap();
453 let sig = decoder::parse_signature("transfer(address,uint256)").unwrap();
454
455 let mut calldata = Vec::new();
457 calldata.extend_from_slice(&sig.selector);
458 let mut addr_word = [0u8; 32];
459 addr_word[31] = 1;
460 calldata.extend_from_slice(&addr_word);
461 let mut amount_word = [0u8; 32];
462 amount_word[30] = 0x03;
463 amount_word[31] = 0xe8;
464 calldata.extend_from_slice(&amount_word);
465
466 let provider = EmptyDataProvider;
467 let addr = "0xdac17f958d2ee523a2206206994597c13d831ec7";
468 let descriptors = wrap_rd(descriptor, 1, addr);
469 let tx = TransactionContext {
470 chain_id: 1,
471 to: addr,
472 calldata: &calldata,
473 value: None,
474 from: None,
475 implementation_address: None,
476 };
477 let result = format_calldata(&descriptors, &tx, &provider).await.unwrap();
478
479 assert_eq!(result.intent, "Transfer tokens");
480 assert_eq!(result.entries.len(), 2);
481
482 if let DisplayEntry::Item(ref item) = result.entries[0] {
483 assert_eq!(item.label, "To");
484 assert_eq!(item.value, "0x0000000000000000000000000000000000000001");
485 } else {
486 panic!("expected Item");
487 }
488
489 if let DisplayEntry::Item(ref item) = result.entries[1] {
490 assert_eq!(item.label, "Amount");
491 assert_eq!(item.value, "1000");
492 } else {
493 panic!("expected Item");
494 }
495 }
496
497 #[tokio::test]
498 async fn test_full_pipeline_with_token_amount() {
499 let json = r#"{
500 "context": {
501 "contract": {
502 "deployments": [
503 { "chainId": 1, "address": "0xdac17f958d2ee523a2206206994597c13d831ec7" }
504 ]
505 }
506 },
507 "metadata": {
508 "owner": "test",
509 "contractName": "Tether USD",
510 "enums": {},
511 "constants": {},
512 "addressBook": {},
513 "maps": {}
514 },
515 "display": {
516 "definitions": {},
517 "formats": {
518 "transfer(address,uint256)": {
519 "intent": "Transfer tokens",
520 "interpolatedIntent": "Send ${@.1} to ${@.0}",
521 "fields": [
522 {
523 "path": "@.0",
524 "label": "To",
525 "format": "addressName"
526 },
527 {
528 "path": "@.1",
529 "label": "Amount",
530 "format": "tokenAmount",
531 "params": {
532 "tokenPath": "@.0"
533 }
534 }
535 ]
536 }
537 }
538 }
539 }"#;
540
541 let descriptor = Descriptor::from_json(json).unwrap();
542 let sig = decoder::parse_signature("transfer(address,uint256)").unwrap();
543
544 let mut calldata = Vec::new();
545 calldata.extend_from_slice(&sig.selector);
546 let token_addr =
548 hex::decode("000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7")
549 .unwrap();
550 calldata.extend_from_slice(&token_addr);
551 let mut amount_word = [0u8; 32];
553 amount_word[29] = 0x0f;
554 amount_word[30] = 0x42;
555 amount_word[31] = 0x40;
556 calldata.extend_from_slice(&amount_word);
557
558 let mut tokens = StaticTokenSource::new();
559 tokens.insert(
560 1,
561 "0xdac17f958d2ee523a2206206994597c13d831ec7",
562 TokenMeta {
563 symbol: "USDT".to_string(),
564 decimals: 6,
565 name: "Tether USD".to_string(),
566 },
567 );
568
569 let addr = "0xdac17f958d2ee523a2206206994597c13d831ec7";
570 let descriptors = wrap_rd(descriptor, 1, addr);
571 let tx = TransactionContext {
572 chain_id: 1,
573 to: addr,
574 calldata: &calldata,
575 value: None,
576 from: None,
577 implementation_address: None,
578 };
579 let result = format_calldata(&descriptors, &tx, &tokens).await.unwrap();
580
581 assert_eq!(result.intent, "Transfer tokens");
582
583 if let DisplayEntry::Item(ref item) = result.entries[0] {
585 assert_eq!(item.label, "To");
586 }
587
588 if let DisplayEntry::Item(ref item) = result.entries[1] {
590 assert_eq!(item.label, "Amount");
591 assert_eq!(item.value, "1 USDT");
592 }
593 }
594
595 #[tokio::test]
596 async fn test_visibility_rules() {
597 let json = r#"{
598 "context": {
599 "contract": {
600 "deployments": [
601 { "chainId": 1, "address": "0xabc" }
602 ]
603 }
604 },
605 "metadata": {
606 "owner": "test",
607 "enums": {},
608 "constants": {},
609 "addressBook": {},
610 "maps": {}
611 },
612 "display": {
613 "definitions": {},
614 "formats": {
615 "foo(uint256,uint256)": {
616 "intent": "Test visibility",
617 "fields": [
618 {
619 "path": "@.0",
620 "label": "Always visible",
621 "format": "number"
622 },
623 {
624 "path": "@.1",
625 "label": "Hidden",
626 "format": "number",
627 "visible": false
628 }
629 ]
630 }
631 }
632 }
633 }"#;
634
635 let descriptor = Descriptor::from_json(json).unwrap();
636 let sig = decoder::parse_signature("foo(uint256,uint256)").unwrap();
637
638 let mut calldata = Vec::new();
639 calldata.extend_from_slice(&sig.selector);
640 calldata.extend_from_slice(&[0u8; 32]); calldata.extend_from_slice(&[0u8; 32]); let descriptors = wrap_rd(descriptor, 1, "0xabc");
644 let tx = TransactionContext {
645 chain_id: 1,
646 to: "0xabc",
647 calldata: &calldata,
648 value: None,
649 from: None,
650 implementation_address: None,
651 };
652 let result = format_calldata(&descriptors, &tx, &EmptyDataProvider)
653 .await
654 .unwrap();
655
656 assert_eq!(result.entries.len(), 1);
658 if let DisplayEntry::Item(ref item) = result.entries[0] {
659 assert_eq!(item.label, "Always visible");
660 }
661 }
662
663 #[tokio::test]
664 async fn test_field_group() {
665 let json = r#"{
666 "context": {
667 "contract": {
668 "deployments": [
669 { "chainId": 1, "address": "0xabc" }
670 ]
671 }
672 },
673 "metadata": {
674 "owner": "test",
675 "enums": {},
676 "constants": {},
677 "addressBook": {},
678 "maps": {}
679 },
680 "display": {
681 "definitions": {},
682 "formats": {
683 "foo(address,uint256)": {
684 "intent": "Test groups",
685 "fields": [
686 {
687 "fieldGroup": {
688 "label": "Transfer Details",
689 "fields": [
690 {
691 "path": "@.0",
692 "label": "Recipient",
693 "format": "address"
694 },
695 {
696 "path": "@.1",
697 "label": "Amount",
698 "format": "number"
699 }
700 ]
701 }
702 }
703 ]
704 }
705 }
706 }
707 }"#;
708
709 let descriptor = Descriptor::from_json(json).unwrap();
710 let sig = decoder::parse_signature("foo(address,uint256)").unwrap();
711
712 let mut calldata = Vec::new();
713 calldata.extend_from_slice(&sig.selector);
714 let mut addr = [0u8; 32];
715 addr[31] = 0x42;
716 calldata.extend_from_slice(&addr);
717 let mut amount = [0u8; 32];
718 amount[31] = 100;
719 calldata.extend_from_slice(&amount);
720
721 let descriptors = wrap_rd(descriptor, 1, "0xabc");
722 let tx = TransactionContext {
723 chain_id: 1,
724 to: "0xabc",
725 calldata: &calldata,
726 value: None,
727 from: None,
728 implementation_address: None,
729 };
730 let result = format_calldata(&descriptors, &tx, &EmptyDataProvider)
731 .await
732 .unwrap();
733
734 assert_eq!(result.entries.len(), 1);
735 if let DisplayEntry::Group { label, items, .. } = &result.entries[0] {
736 assert_eq!(label, "Transfer Details");
737 assert_eq!(items.len(), 2);
738 assert_eq!(items[0].label, "Recipient");
739 assert_eq!(items[1].label, "Amount");
740 assert_eq!(items[1].value, "100");
741 } else {
742 panic!("expected Group");
743 }
744 }
745
746 #[tokio::test]
747 async fn test_maps_lookup() {
748 let json = r#"{
749 "context": {
750 "contract": {
751 "deployments": [
752 { "chainId": 1, "address": "0xabc" }
753 ]
754 }
755 },
756 "metadata": {
757 "owner": "test",
758 "enums": {},
759 "constants": {},
760 "addressBook": {},
761 "maps": {
762 "orderTypes": {
763 "entries": {
764 "0": "Market",
765 "1": "Limit",
766 "2": "Stop"
767 }
768 }
769 }
770 },
771 "display": {
772 "definitions": {},
773 "formats": {
774 "placeOrder(uint256)": {
775 "intent": "Place order",
776 "fields": [
777 {
778 "path": "@.0",
779 "label": "Order Type",
780 "params": {
781 "mapReference": "orderTypes"
782 }
783 }
784 ]
785 }
786 }
787 }
788 }"#;
789
790 let descriptor = Descriptor::from_json(json).unwrap();
791 let sig = decoder::parse_signature("placeOrder(uint256)").unwrap();
792
793 let mut calldata = Vec::new();
794 calldata.extend_from_slice(&sig.selector);
795 let mut word = [0u8; 32];
796 word[31] = 1; calldata.extend_from_slice(&word);
798
799 let descriptors = wrap_rd(descriptor, 1, "0xabc");
800 let tx = TransactionContext {
801 chain_id: 1,
802 to: "0xabc",
803 calldata: &calldata,
804 value: None,
805 from: None,
806 implementation_address: None,
807 };
808 let result = format_calldata(&descriptors, &tx, &EmptyDataProvider)
809 .await
810 .unwrap();
811
812 if let DisplayEntry::Item(ref item) = result.entries[0] {
813 assert_eq!(item.label, "Order Type");
814 assert_eq!(item.value, "Limit");
815 } else {
816 panic!("expected Item");
817 }
818 }
819
820 #[tokio::test]
821 async fn test_stakeweight_increase_unlock_time() {
822 let json = r#"{
823 "context": {
824 "contract": {
825 "deployments": [
826 { "chainId": 10, "address": "0x521B4C065Bbdbe3E20B3727340730936912DfA46" }
827 ]
828 }
829 },
830 "metadata": {
831 "owner": "WalletConnect",
832 "contractName": "StakeWeight",
833 "enums": {},
834 "constants": {},
835 "addressBook": {},
836 "maps": {}
837 },
838 "display": {
839 "definitions": {},
840 "formats": {
841 "increaseUnlockTime(uint256)": {
842 "intent": "Increase Unlock Time",
843 "interpolatedIntent": "Increase unlock time to ${@.0}",
844 "fields": [
845 {
846 "path": "@.0",
847 "label": "New Unlock Time",
848 "format": "date"
849 }
850 ]
851 }
852 }
853 }
854 }"#;
855
856 let descriptor = Descriptor::from_json(json).unwrap();
857 let calldata =
859 hex::decode("7c616fe6000000000000000000000000000000000000000000000000000000006945563d")
860 .unwrap();
861
862 let addr = "0x521B4C065Bbdbe3E20B3727340730936912DfA46";
863 let descriptors = wrap_rd(descriptor, 10, addr);
864 let tx = TransactionContext {
865 chain_id: 10,
866 to: addr,
867 calldata: &calldata,
868 value: None,
869 from: None,
870 implementation_address: None,
871 };
872 let result = format_calldata(&descriptors, &tx, &EmptyDataProvider)
873 .await
874 .unwrap();
875
876 assert_eq!(result.intent, "Increase Unlock Time");
877 assert_eq!(result.entries.len(), 1);
878 if let DisplayEntry::Item(ref item) = result.entries[0] {
879 assert_eq!(item.label, "New Unlock Time");
880 assert_eq!(item.value, "2025-12-19 13:42:21 UTC");
881 } else {
882 panic!("expected Item");
883 }
884 assert_eq!(
885 result.interpolated_intent.as_deref(),
886 Some("Increase unlock time to 2025-12-19 13:42:21 UTC")
887 );
888 assert!(result.diagnostics().is_empty());
889 }
890
891 #[tokio::test]
892 async fn test_eip712_format() {
893 let json = r#"{
894 "context": {
895 "eip712": {
896 "deployments": [
897 { "chainId": 1, "address": "0xabc" }
898 ]
899 }
900 },
901 "metadata": {
902 "owner": "test",
903 "enums": {},
904 "constants": {},
905 "addressBook": {},
906 "maps": {}
907 },
908 "display": {
909 "definitions": {},
910 "formats": {
911 "Permit(address spender,uint256 value)": {
912 "intent": "Permit token spending",
913 "fields": [
914 {
915 "path": "spender",
916 "label": "Spender",
917 "format": "address"
918 },
919 {
920 "path": "value",
921 "label": "Amount",
922 "format": "number"
923 }
924 ]
925 }
926 }
927 }
928 }"#;
929
930 let descriptor = Descriptor::from_json(json).unwrap();
931 let typed_data = eip712::TypedData {
932 types: std::collections::HashMap::from([(
933 "Permit".to_string(),
934 vec![
935 eip712::TypedDataField {
936 name: "spender".to_string(),
937 field_type: "address".to_string(),
938 },
939 eip712::TypedDataField {
940 name: "value".to_string(),
941 field_type: "uint256".to_string(),
942 },
943 ],
944 )]),
945 primary_type: "Permit".to_string(),
946 domain: eip712::TypedDataDomain {
947 name: Some("USDT".to_string()),
948 version: Some("1".to_string()),
949 chain_id: Some(1),
950 verifying_contract: Some("0xabc".to_string()),
951 salt: None,
952 extra: std::collections::HashMap::new(),
953 },
954 container: None,
955 message: serde_json::json!({
956 "spender": "0x1234567890123456789012345678901234567890",
957 "value": "1000000"
958 }),
959 };
960
961 let descriptors = wrap_rd(descriptor, 1, "0xabc");
962 let result = format_typed_data(&descriptors, &typed_data, &EmptyDataProvider)
963 .await
964 .unwrap();
965 assert_eq!(result.intent, "Permit token spending");
966 assert_eq!(result.entries.len(), 2);
967
968 if let DisplayEntry::Item(ref item) = result.entries[0] {
969 assert_eq!(item.label, "Spender");
970 assert_eq!(item.value, "0x1234567890123456789012345678901234567890");
971 }
972
973 if let DisplayEntry::Item(ref item) = result.entries[1] {
974 assert_eq!(item.label, "Amount");
975 assert_eq!(item.value, "1000000");
976 }
977 }
978
979 #[tokio::test]
980 async fn test_proxy_implementation_address() {
981 let descriptor = Descriptor::from_json(test_descriptor_json()).unwrap();
982 let sig = decoder::parse_signature("transfer(address,uint256)").unwrap();
983
984 let mut calldata = Vec::new();
985 calldata.extend_from_slice(&sig.selector);
986 let mut addr_word = [0u8; 32];
987 addr_word[31] = 1;
988 calldata.extend_from_slice(&addr_word);
989 let mut amount_word = [0u8; 32];
990 amount_word[30] = 0x03;
991 amount_word[31] = 0xe8;
992 calldata.extend_from_slice(&amount_word);
993
994 let impl_addr = "0xdac17f958d2ee523a2206206994597c13d831ec7";
998 let proxy_addr = "0x1111111111111111111111111111111111111111";
999 let descriptors = wrap_rd(descriptor, 1, impl_addr);
1000 let tx = TransactionContext {
1001 chain_id: 1,
1002 to: proxy_addr,
1003 calldata: &calldata,
1004 value: None,
1005 from: None,
1006 implementation_address: Some(impl_addr),
1007 };
1008 let result = format_calldata(&descriptors, &tx, &EmptyDataProvider)
1009 .await
1010 .unwrap();
1011
1012 assert_eq!(result.intent, "Transfer tokens");
1014 assert_eq!(result.entries.len(), 2);
1015
1016 if let DisplayEntry::Item(ref item) = result.entries[0] {
1018 assert_eq!(item.label, "To");
1019 }
1020 }
1021
1022 #[tokio::test]
1023 async fn test_format_calldata_empty_descriptors_returns_raw_fallback() {
1024 let calldata =
1025 hex::decode("a9059cbb0000000000000000000000000000000000000000000000000000000000000001")
1026 .unwrap();
1027 let tx = TransactionContext {
1028 chain_id: 1,
1029 to: "0x0000000000000000000000000000000000000001",
1030 calldata: &calldata,
1031 value: None,
1032 from: None,
1033 implementation_address: None,
1034 };
1035
1036 let result = format_calldata(&[], &tx, &EmptyDataProvider)
1037 .await
1038 .expect("empty descriptors should fall back");
1039 assert!(result.intent.starts_with("Unknown function 0xa9059cbb"));
1040 }
1041
1042 #[tokio::test]
1043 async fn test_format_calldata_errors_when_descriptor_deployment_does_not_match() {
1044 let descriptor = Descriptor::from_json(test_descriptor_json()).unwrap();
1045 let sig = decoder::parse_signature("transfer(address,uint256)").unwrap();
1046
1047 let mut calldata = Vec::new();
1048 calldata.extend_from_slice(&sig.selector);
1049 calldata.extend_from_slice(&[0u8; 32]);
1050 calldata.extend_from_slice(&[0u8; 32]);
1051
1052 let descriptors = wrap_rd(descriptor, 1, "0xdac17f958d2ee523a2206206994597c13d831ec7");
1053 let tx = TransactionContext {
1054 chain_id: 1,
1055 to: "0x0000000000000000000000000000000000000001",
1056 calldata: &calldata,
1057 value: None,
1058 from: None,
1059 implementation_address: None,
1060 };
1061
1062 let err = format_calldata(&descriptors, &tx, &EmptyDataProvider)
1063 .await
1064 .expect_err("mismatched deployment should error");
1065 assert!(err
1066 .to_string()
1067 .contains("no outer descriptor matches chain_id=1 address=0x0000000000000000000000000000000000000001"));
1068 }
1069}