1use num_bigint::BigUint;
4use serde::{de, Deserialize, Deserializer, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DescriptorDisplay {
10 #[serde(default)]
12 pub definitions: HashMap<String, DisplayField>,
13
14 pub formats: HashMap<String, DisplayFormat>,
18}
19
20pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct DisplayFormat {
64 #[serde(rename = "$id")]
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub id: Option<String>,
68
69 #[serde(deserialize_with = "deserialize_intent")]
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub intent: Option<serde_json::Value>,
73
74 #[serde(rename = "interpolatedIntent")]
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub interpolated_intent: Option<String>,
78
79 #[serde(default)]
81 pub fields: Vec<DisplayField>,
82
83 #[serde(default)]
85 pub excluded: Vec<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(untagged)]
91#[allow(clippy::large_enum_variant)]
92pub enum DisplayField {
93 Reference {
98 #[serde(rename = "$ref")]
99 reference: String,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 path: Option<String>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 params: Option<FormatParams>,
108
109 #[serde(default = "default_visible")]
111 visible: VisibleRule,
112 },
113
114 Group {
116 #[serde(rename = "fieldGroup")]
117 field_group: FieldGroup,
118 },
119
120 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 Simple {
136 #[serde(skip_serializing_if = "Option::is_none")]
138 path: Option<String>,
139
140 label: String,
141
142 #[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 #[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#[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182#[serde(rename_all = "lowercase")]
183pub enum Iteration {
184 #[default]
185 Sequential,
186 Bundled,
187}
188
189#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191#[serde(untagged)]
192pub enum VisibleRule {
193 Bool(bool),
195
196 Named(VisibleLiteral),
198
199 Condition(VisibleCondition),
201
202 #[default]
204 Always,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
209#[serde(rename_all = "lowercase")]
210pub enum VisibleLiteral {
211 Always,
212 Never,
213 Optional,
214}
215
216#[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#[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 InteroperableAddressName,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct FormatParams {
268 #[serde(rename = "tokenPath")]
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub token_path: Option<String>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub token: Option<String>,
276
277 #[serde(rename = "nativeCurrencyAddress")]
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub native_currency_address: Option<NativeCurrencyAddress>,
282
283 #[serde(rename = "chainId")]
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub chain_id: Option<u64>,
287
288 #[serde(rename = "chainIdPath")]
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub chain_id_path: Option<String>,
292
293 #[serde(rename = "enumPath")]
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub enum_path: Option<String>,
297
298 #[serde(rename = "$ref")]
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub ref_path: Option<String>,
302
303 #[serde(rename = "mapReference")]
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub map_reference: Option<String>,
307
308 #[serde(skip_serializing_if = "Option::is_none")]
311 pub threshold: Option<String>,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub message: Option<String>,
316
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub base: Option<String>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub decimals: Option<u8>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub prefix: Option<bool>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub encryption: Option<EncryptionParams>,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub encoding: Option<String>,
336
337 #[serde(rename = "selectorPath")]
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub selector_path: Option<String>,
341
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub selector: Option<String>,
345
346 #[serde(rename = "calleePath")]
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub callee_path: Option<String>,
350
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub callee: Option<String>,
354
355 #[serde(rename = "amountPath")]
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub amount_path: Option<String>,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub amount: Option<UintLiteral>,
363
364 #[serde(rename = "spenderPath")]
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub spender_path: Option<String>,
368
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub spender: Option<String>,
372
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub types: Option<Vec<String>>,
376
377 #[serde(skip_serializing_if = "Option::is_none")]
379 pub sources: Option<Vec<String>>,
380
381 #[serde(rename = "senderAddress")]
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub sender_address: Option<SenderAddress>,
385
386 #[serde(rename = "collectionPath")]
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub collection_path: Option<String>,
390
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub collection: Option<String>,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(untagged)]
401pub enum NativeCurrencyAddress {
402 Single(String),
403 Multiple(Vec<String>),
404}
405
406impl NativeCurrencyAddress {
407 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#[derive(Debug, Clone, Serialize, Deserialize)]
426#[serde(untagged)]
427pub enum SenderAddress {
428 Single(String),
429 Multiple(Vec<String>),
430}
431
432#[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#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct EncryptionParams {
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub scheme: Option<String>,
466
467 #[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}