Skip to main content

clear_signing/types/
display.rs

1//! Display configuration types: field formats, visibility rules, and layout groups.
2
3use num_bigint::BigUint;
4use serde::{de, Deserialize, Deserializer, Serialize};
5use std::collections::HashMap;
6
7/// Top-level display section of a descriptor.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DescriptorDisplay {
10    /// Reusable field definitions that can be referenced via `$ref`.
11    #[serde(default)]
12    pub definitions: HashMap<String, DisplayField>,
13
14    /// Map of format key → display format.
15    /// For calldata: key is function signature like `"transfer(address,uint256)"`.
16    /// For EIP-712: key is primary type name.
17    pub formats: HashMap<String, DisplayFormat>,
18}
19
20/// Extract an intent string from a validated intent value.
21pub fn intent_as_string(val: &serde_json::Value) -> String {
22    match val {
23        serde_json::Value::String(s) => s.clone(),
24        serde_json::Value::Object(obj) => obj
25            .iter()
26            .map(|(label, value)| format!("{label}: {}", value.as_str().unwrap_or_default()))
27            .collect::<Vec<_>>()
28            .join(", "),
29        _ => val.to_string(),
30    }
31}
32
33fn deserialize_intent<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
34where
35    D: Deserializer<'de>,
36{
37    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
38    let Some(value) = value else {
39        return Ok(None);
40    };
41
42    match &value {
43        serde_json::Value::String(_) => Ok(Some(value)),
44        serde_json::Value::Object(obj) => {
45            for (key, entry) in obj {
46                if !entry.is_string() {
47                    return Err(de::Error::custom(format!(
48                        "intent object value for key '{}' must be a string",
49                        key
50                    )));
51                }
52            }
53            Ok(Some(value))
54        }
55        _ => Err(de::Error::custom(
56            "intent must be a string or a flat object of string values",
57        )),
58    }
59}
60
61/// A single display format for a function or message type.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct DisplayFormat {
64    /// Optional format identifier (v2).
65    #[serde(rename = "$id")]
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub id: Option<String>,
68
69    /// Human-readable intent label (string or object per spec).
70    #[serde(deserialize_with = "deserialize_intent")]
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub intent: Option<serde_json::Value>,
73
74    /// Intent with `${path}` or `{name}` template variables for interpolation.
75    #[serde(rename = "interpolatedIntent")]
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub interpolated_intent: Option<String>,
78
79    /// Ordered list of fields to display.
80    #[serde(default)]
81    pub fields: Vec<DisplayField>,
82
83    /// Deprecated in v2 — list of excluded paths.
84    #[serde(default)]
85    pub excluded: Vec<String>,
86}
87
88/// A display field — can be a simple field, a field group, or a reference.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(untagged)]
91#[allow(clippy::large_enum_variant)]
92pub enum DisplayField {
93    /// A reference to a definition: `{ "$ref": "$.display.definitions.foo", "path": "...", ... }`.
94    ///
95    /// Per ERC-7730 spec, a reference object carries `$ref` plus the field's own
96    /// `path`, optional `params` (which override definition params), and `visible`.
97    Reference {
98        #[serde(rename = "$ref")]
99        reference: String,
100
101        /// Path to resolve in decoded arguments (from the referencing field).
102        #[serde(skip_serializing_if = "Option::is_none")]
103        path: Option<String>,
104
105        /// Params that override/extend the definition's params.
106        #[serde(skip_serializing_if = "Option::is_none")]
107        params: Option<FormatParams>,
108
109        /// Visibility rule (from the referencing field).
110        #[serde(default = "default_visible")]
111        visible: VisibleRule,
112    },
113
114    /// A grouped set of fields (v2): `{ "fieldGroup": { ... } }`.
115    Group {
116        #[serde(rename = "fieldGroup")]
117        field_group: FieldGroup,
118    },
119
120    /// Direct spec group object: `{ "path": "...", "label": "...", "fields": [...] }`.
121    Scope {
122        #[serde(skip_serializing_if = "Option::is_none")]
123        path: Option<String>,
124
125        #[serde(skip_serializing_if = "Option::is_none")]
126        label: Option<String>,
127
128        #[serde(default)]
129        iteration: Iteration,
130
131        fields: Vec<DisplayField>,
132    },
133
134    /// A simple field with path, label, format, etc.
135    Simple {
136        /// Path to resolve in decoded arguments. Optional when `value` is provided.
137        #[serde(skip_serializing_if = "Option::is_none")]
138        path: Option<String>,
139
140        label: String,
141
142        /// Literal constant value (alternative to `path`).
143        #[serde(skip_serializing_if = "Option::is_none")]
144        value: Option<String>,
145
146        #[serde(skip_serializing_if = "Option::is_none")]
147        format: Option<FieldFormat>,
148
149        #[serde(skip_serializing_if = "Option::is_none")]
150        params: Option<FormatParams>,
151
152        /// Separator string for array-typed values.
153        #[serde(skip_serializing_if = "Option::is_none")]
154        separator: Option<String>,
155
156        #[serde(default = "default_visible")]
157        visible: VisibleRule,
158    },
159}
160
161fn default_visible() -> VisibleRule {
162    VisibleRule::Always
163}
164
165/// A field group — replaces v1's `nestedFields`.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct FieldGroup {
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub path: Option<String>,
170
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub label: Option<String>,
173
174    #[serde(default)]
175    pub iteration: Iteration,
176
177    pub fields: Vec<DisplayField>,
178}
179
180/// How grouped fields should be iterated for display.
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182#[serde(rename_all = "lowercase")]
183pub enum Iteration {
184    #[default]
185    Sequential,
186    Bundled,
187}
188
189/// Visibility rule for a field.
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191#[serde(untagged)]
192pub enum VisibleRule {
193    /// Boolean shorthand: true = Always, false = Never.
194    Bool(bool),
195
196    /// String shorthand: "always", "never", or "optional".
197    Named(VisibleLiteral),
198
199    /// Conditional visibility.
200    Condition(VisibleCondition),
201
202    /// Default: always visible.
203    #[default]
204    Always,
205}
206
207/// Named visibility literals accepted by the current spec.
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
209#[serde(rename_all = "lowercase")]
210pub enum VisibleLiteral {
211    Always,
212    Never,
213    Optional,
214}
215
216/// Conditional visibility: `ifNotIn` or `mustMatch`.
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct VisibleCondition {
219    #[serde(rename = "ifNotIn")]
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub if_not_in: Option<Vec<serde_json::Value>>,
222
223    #[serde(rename = "mustMatch", alias = "mustBe")]
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub must_match: Option<Vec<serde_json::Value>>,
226}
227
228impl VisibleCondition {
229    pub fn hides_for_if_not_in(&self, value: &serde_json::Value) -> bool {
230        self.if_not_in
231            .as_ref()
232            .is_some_and(|excluded| excluded.contains(value))
233    }
234
235    pub fn matches_must_match(&self, value: &serde_json::Value) -> bool {
236        self.must_match
237            .as_ref()
238            .is_none_or(|required| required.contains(value))
239    }
240}
241
242/// Field format types.
243#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub enum FieldFormat {
246    TokenAmount,
247    Amount,
248    Date,
249    #[serde(rename = "enum")]
250    Enum,
251    Address,
252    AddressName,
253    Number,
254    Raw,
255    TokenTicker,
256    ChainId,
257    Calldata,
258    NftName,
259    Duration,
260    Unit,
261    /// ERC-7930 interoperable address format.
262    InteroperableAddressName,
263}
264
265/// Format parameters — varies by format type.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct FormatParams {
268    /// Token address path for tokenAmount/tokenTicker (resolved from calldata).
269    #[serde(rename = "tokenPath")]
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub token_path: Option<String>,
272
273    /// Static token address or `$.metadata.constants.*` ref for tokenAmount/tokenTicker.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub token: Option<String>,
276
277    /// Native currency address — single address or array of addresses/constant refs.
278    /// Per spec: "Either a string or an array of strings."
279    #[serde(rename = "nativeCurrencyAddress")]
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub native_currency_address: Option<NativeCurrencyAddress>,
282
283    /// Static chain ID for cross-chain token resolution.
284    #[serde(rename = "chainId")]
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub chain_id: Option<u64>,
287
288    /// Dynamic chain ID path from calldata.
289    #[serde(rename = "chainIdPath")]
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub chain_id_path: Option<String>,
292
293    /// Enum lookup key in metadata.enums.
294    #[serde(rename = "enumPath")]
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub enum_path: Option<String>,
297
298    /// `$ref` enum reference path (v2): e.g., `"$.metadata.enums.interestRateMode"`.
299    #[serde(rename = "$ref")]
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub ref_path: Option<String>,
302
303    /// Map reference key in metadata.maps.
304    #[serde(rename = "mapReference")]
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub map_reference: Option<String>,
307
308    /// Threshold for max-amount display (v2).
309    /// Value or `"$.metadata.constants.max"` reference.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub threshold: Option<String>,
312
313    /// Message to display when amount >= threshold (e.g., "All", "Max").
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub message: Option<String>,
316
317    /// Unit base symbol (e.g., "%", "bps", "h") for the `unit` format.
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub base: Option<String>,
320
321    /// Decimal places for the `unit` format (default 0).
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub decimals: Option<u8>,
324
325    /// Whether to use SI prefix notation for the `unit` format.
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub prefix: Option<bool>,
328
329    /// Encryption parameters.
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub encryption: Option<EncryptionParams>,
332
333    /// Date encoding: `"timestamp"` (default) or `"blockheight"`.
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub encoding: Option<String>,
336
337    /// Path to resolve which selector to use for nested calldata decoding.
338    #[serde(rename = "selectorPath")]
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub selector_path: Option<String>,
341
342    /// Constant selector override for nested calldata decoding.
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub selector: Option<String>,
345
346    /// Path to the callee address for nested calldata (e.g., "to").
347    #[serde(rename = "calleePath")]
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub callee_path: Option<String>,
350
351    /// Constant callee address for nested calldata.
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub callee: Option<String>,
354
355    /// Path to the value amount for nested calldata (injected as `@.value` in inner context).
356    #[serde(rename = "amountPath")]
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub amount_path: Option<String>,
359
360    /// Constant native amount for nested calldata.
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub amount: Option<UintLiteral>,
363
364    /// Path to the spender/from address for nested calldata (injected as `@.from` in inner context).
365    #[serde(rename = "spenderPath")]
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub spender_path: Option<String>,
368
369    /// Constant spender/from address for nested calldata.
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub spender: Option<String>,
372
373    /// Address types for addressName format (spec: "eoa", "contract", etc.).
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub types: Option<Vec<String>>,
376
377    /// Trusted name sources for addressName format (spec: "ens", "local").
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub sources: Option<Vec<String>>,
380
381    /// Sender address check for addressName format.
382    #[serde(rename = "senderAddress")]
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub sender_address: Option<SenderAddress>,
385
386    /// Path to the collection address for nftName format.
387    #[serde(rename = "collectionPath")]
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub collection_path: Option<String>,
390
391    /// Constant collection address for nftName format.
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub collection: Option<String>,
394}
395
396/// Native currency address — single address or array of addresses/constant refs.
397/// Per ERC-7730 spec: "Either a string or an array of strings."
398/// Values may be `$.metadata.constants.xxx` references resolved at comparison time.
399#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(untagged)]
401pub enum NativeCurrencyAddress {
402    Single(String),
403    Multiple(Vec<String>),
404}
405
406impl NativeCurrencyAddress {
407    /// Check if `addr` matches any native currency address, resolving `$.metadata.constants.*` refs.
408    pub fn matches(&self, addr: &str, constants: &HashMap<String, serde_json::Value>) -> bool {
409        let items: Vec<&str> = match self {
410            NativeCurrencyAddress::Single(s) => vec![s.as_str()],
411            NativeCurrencyAddress::Multiple(v) => v.iter().map(|s| s.as_str()).collect(),
412        };
413        items.iter().any(|item| {
414            let resolved = if let Some(key) = item.strip_prefix("$.metadata.constants.") {
415                constants.get(key).and_then(|v| v.as_str()).unwrap_or(item)
416            } else {
417                item
418            };
419            resolved.eq_ignore_ascii_case(addr)
420        })
421    }
422}
423
424/// Sender address — can be a single address or an array of addresses/paths.
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[serde(untagged)]
427pub enum SenderAddress {
428    Single(String),
429    Multiple(Vec<String>),
430}
431
432/// Unsigned integer literal for descriptor params.
433#[derive(Debug, Clone, Serialize, Deserialize)]
434#[serde(untagged)]
435pub enum UintLiteral {
436    Number(u64),
437    String(String),
438}
439
440impl UintLiteral {
441    pub fn to_biguint(&self) -> Option<BigUint> {
442        match self {
443            UintLiteral::Number(value) => Some(BigUint::from(*value)),
444            UintLiteral::String(value) => {
445                let trimmed = value.trim();
446                if let Some(hex) = trimmed
447                    .strip_prefix("0x")
448                    .or_else(|| trimmed.strip_prefix("0X"))
449                {
450                    let bytes = hex::decode(hex).ok()?;
451                    Some(BigUint::from_bytes_be(&bytes))
452                } else {
453                    trimmed.parse::<BigUint>().ok()
454                }
455            }
456        }
457    }
458}
459
460/// Encryption parameters for encrypted fields.
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct EncryptionParams {
463    /// Encryption scheme identifier.
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub scheme: Option<String>,
466
467    /// Type of the plaintext content.
468    #[serde(rename = "plaintextType")]
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub plaintext_type: Option<String>,
471
472    #[serde(rename = "fallbackLabel")]
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub fallback_label: Option<String>,
475}