use num_bigint::BigUint;
use serde::{de, Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DescriptorDisplay {
#[serde(default)]
pub definitions: HashMap<String, DisplayField>,
pub formats: HashMap<String, DisplayFormat>,
}
pub fn intent_as_string(val: &serde_json::Value) -> String {
match val {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Object(obj) => obj
.iter()
.map(|(label, value)| format!("{label}: {}", value.as_str().unwrap_or_default()))
.collect::<Vec<_>>()
.join(", "),
_ => val.to_string(),
}
}
fn deserialize_intent<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
let Some(value) = value else {
return Ok(None);
};
match &value {
serde_json::Value::String(_) => Ok(Some(value)),
serde_json::Value::Object(obj) => {
for (key, entry) in obj {
if !entry.is_string() {
return Err(de::Error::custom(format!(
"intent object value for key '{}' must be a string",
key
)));
}
}
Ok(Some(value))
}
_ => Err(de::Error::custom(
"intent must be a string or a flat object of string values",
)),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayFormat {
#[serde(rename = "$id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(deserialize_with = "deserialize_intent")]
#[serde(skip_serializing_if = "Option::is_none")]
pub intent: Option<serde_json::Value>,
#[serde(rename = "interpolatedIntent")]
#[serde(skip_serializing_if = "Option::is_none")]
pub interpolated_intent: Option<String>,
#[serde(default)]
pub fields: Vec<DisplayField>,
#[serde(default)]
pub excluded: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum DisplayField {
Reference {
#[serde(rename = "$ref")]
reference: String,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
params: Option<FormatParams>,
#[serde(default = "default_visible")]
visible: VisibleRule,
},
Group {
#[serde(rename = "fieldGroup")]
field_group: FieldGroup,
},
Scope {
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
label: Option<String>,
#[serde(default)]
iteration: Iteration,
fields: Vec<DisplayField>,
},
Simple {
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
label: String,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
format: Option<FieldFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
params: Option<FormatParams>,
#[serde(skip_serializing_if = "Option::is_none")]
separator: Option<String>,
#[serde(default = "default_visible")]
visible: VisibleRule,
},
}
fn default_visible() -> VisibleRule {
VisibleRule::Always
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldGroup {
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default)]
pub iteration: Iteration,
pub fields: Vec<DisplayField>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Iteration {
#[default]
Sequential,
Bundled,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VisibleRule {
Bool(bool),
Named(VisibleLiteral),
Condition(VisibleCondition),
#[default]
Always,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VisibleLiteral {
Always,
Never,
Optional,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VisibleCondition {
#[serde(rename = "ifNotIn")]
#[serde(skip_serializing_if = "Option::is_none")]
pub if_not_in: Option<Vec<serde_json::Value>>,
#[serde(rename = "mustMatch", alias = "mustBe")]
#[serde(skip_serializing_if = "Option::is_none")]
pub must_match: Option<Vec<serde_json::Value>>,
}
impl VisibleCondition {
pub fn hides_for_if_not_in(&self, value: &serde_json::Value) -> bool {
self.if_not_in
.as_ref()
.is_some_and(|excluded| excluded.contains(value))
}
pub fn matches_must_match(&self, value: &serde_json::Value) -> bool {
self.must_match
.as_ref()
.is_none_or(|required| required.contains(value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FieldFormat {
TokenAmount,
Amount,
Date,
#[serde(rename = "enum")]
Enum,
Address,
AddressName,
Number,
Raw,
TokenTicker,
ChainId,
Calldata,
NftName,
Duration,
Unit,
InteroperableAddressName,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormatParams {
#[serde(rename = "tokenPath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub token_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(rename = "nativeCurrencyAddress")]
#[serde(skip_serializing_if = "Option::is_none")]
pub native_currency_address: Option<NativeCurrencyAddress>,
#[serde(rename = "chainId")]
#[serde(skip_serializing_if = "Option::is_none")]
pub chain_id: Option<u64>,
#[serde(rename = "chainIdPath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub chain_id_path: Option<String>,
#[serde(rename = "enumPath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_path: Option<String>,
#[serde(rename = "$ref")]
#[serde(skip_serializing_if = "Option::is_none")]
pub ref_path: Option<String>,
#[serde(rename = "mapReference")]
#[serde(skip_serializing_if = "Option::is_none")]
pub map_reference: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub threshold: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decimals: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encryption: Option<EncryptionParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding: Option<String>,
#[serde(rename = "selectorPath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub selector_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
#[serde(rename = "calleePath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub callee_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub callee: Option<String>,
#[serde(rename = "amountPath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub amount_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<UintLiteral>,
#[serde(rename = "spenderPath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub spender_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spender: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub types: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sources: Option<Vec<String>>,
#[serde(rename = "senderAddress")]
#[serde(skip_serializing_if = "Option::is_none")]
pub sender_address: Option<SenderAddress>,
#[serde(rename = "collectionPath")]
#[serde(skip_serializing_if = "Option::is_none")]
pub collection_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub collection: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum NativeCurrencyAddress {
Single(String),
Multiple(Vec<String>),
}
impl NativeCurrencyAddress {
pub fn matches(&self, addr: &str, constants: &HashMap<String, serde_json::Value>) -> bool {
let items: Vec<&str> = match self {
NativeCurrencyAddress::Single(s) => vec![s.as_str()],
NativeCurrencyAddress::Multiple(v) => v.iter().map(|s| s.as_str()).collect(),
};
items.iter().any(|item| {
let resolved = if let Some(key) = item.strip_prefix("$.metadata.constants.") {
constants.get(key).and_then(|v| v.as_str()).unwrap_or(item)
} else {
item
};
resolved.eq_ignore_ascii_case(addr)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SenderAddress {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UintLiteral {
Number(u64),
String(String),
}
impl UintLiteral {
pub fn to_biguint(&self) -> Option<BigUint> {
match self {
UintLiteral::Number(value) => Some(BigUint::from(*value)),
UintLiteral::String(value) => {
let trimmed = value.trim();
if let Some(hex) = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
let bytes = hex::decode(hex).ok()?;
Some(BigUint::from_bytes_be(&bytes))
} else {
trimmed.parse::<BigUint>().ok()
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncryptionParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub scheme: Option<String>,
#[serde(rename = "plaintextType")]
#[serde(skip_serializing_if = "Option::is_none")]
pub plaintext_type: Option<String>,
#[serde(rename = "fallbackLabel")]
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback_label: Option<String>,
}