Skip to main content

hypercall_api/directives/
typed_data.rs

1use crate::directives::json_types::JsonU64;
2use crate::directives::models::ParsedAction;
3use crate::error::ApiError;
4use alloy::primitives::Address;
5use hypercall_types::{
6    directives::{self, ActionKey, DomainKind, TypedDataSpec},
7    WalletAddress,
8};
9use serde::Serialize;
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct DomainJson {
15    pub name: String,
16    pub version: String,
17    pub chain_id: JsonU64,
18    pub verifying_contract: WalletAddress,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct TypeField {
23    pub name: &'static str,
24    #[serde(rename = "type")]
25    pub kind: &'static str,
26}
27
28#[derive(Debug, Clone, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct TypedDataMessage {
31    pub account: WalletAddress,
32    pub nonce: JsonU64,
33    pub action: sonic_rs::Value,
34}
35
36#[derive(Debug, Clone, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct TypedDataResponse {
39    pub domain: DomainJson,
40    #[serde(rename = "primaryType")]
41    pub primary_type: String,
42    pub types: BTreeMap<String, Vec<TypeField>>,
43    pub message: TypedDataMessage,
44}
45
46pub fn domain_for(kind: DomainKind, chain_id: u64) -> Result<DomainJson, ApiError> {
47    match chain_id {
48        directives::HYPERCALL_TESTNET_CHAIN_ID | directives::HYPERCALL_MAINNET_CHAIN_ID => {}
49        _ => {
50            return Err(ApiError::internal_error(format!(
51                "Unsupported directive chain id: {}",
52                chain_id
53            )))
54        }
55    }
56
57    let name = match kind {
58        DomainKind::Api => directives::HYPERCALL_API_DOMAIN_NAME,
59        DomainKind::Manager => directives::HYPERCALL_MANAGER_DOMAIN_NAME,
60        DomainKind::Rsm => directives::HYPERCALL_RSM_DOMAIN_NAME,
61    };
62
63    Ok(DomainJson {
64        name: name.to_string(),
65        version: directives::HYPERCALL_DOMAIN_VERSION.to_string(),
66        chain_id: JsonU64(chain_id),
67        verifying_contract: WalletAddress::from(Address::ZERO),
68    })
69}
70
71fn eip712_domain_type() -> Vec<TypeField> {
72    vec![
73        TypeField {
74            name: "name",
75            kind: "string",
76        },
77        TypeField {
78            name: "version",
79            kind: "string",
80        },
81        TypeField {
82            name: "chainId",
83            kind: "uint256",
84        },
85        TypeField {
86            name: "verifyingContract",
87            kind: "address",
88        },
89    ]
90}
91
92fn typed_data_types(
93    primary_type: &str,
94    typed_data: TypedDataSpec,
95) -> BTreeMap<String, Vec<TypeField>> {
96    let mut types = BTreeMap::new();
97    types.insert("EIP712Domain".to_string(), eip712_domain_type());
98    types.insert(
99        typed_data.inner_type.to_string(),
100        typed_data
101            .inner_fields
102            .iter()
103            .map(|field| TypeField {
104                name: field.name,
105                kind: field.kind,
106            })
107            .collect(),
108    );
109    types.insert(
110        primary_type.to_string(),
111        vec![
112            TypeField {
113                name: "account",
114                kind: "address",
115            },
116            TypeField {
117                name: "nonce",
118                kind: "uint64",
119            },
120            TypeField {
121                name: "action",
122                kind: typed_data.inner_type,
123            },
124        ],
125    );
126    types
127}
128
129pub fn build_typed_data(
130    action_key: ActionKey,
131    account: WalletAddress,
132    nonce: JsonU64,
133    action: &ParsedAction,
134    chain_id: u64,
135) -> Result<TypedDataResponse, ApiError> {
136    let spec = action_key.spec();
137    let typed_data = spec.typed_data.ok_or_else(|| {
138        ApiError::internal_error(format!(
139            "Typed data metadata missing for action key: {}",
140            action_key.as_str()
141        ))
142    })?;
143
144    let domain = domain_for(spec.domain, chain_id)?;
145    let types = typed_data_types(spec.primary_type, typed_data);
146    let message = TypedDataMessage {
147        account,
148        nonce,
149        action: action.to_json_value(),
150    };
151
152    Ok(TypedDataResponse {
153        domain,
154        primary_type: spec.primary_type.to_string(),
155        types,
156        message,
157    })
158}