hypercall_api/directives/
typed_data.rs1use 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}