1use axum::http::{Method, StatusCode};
2use hypercall_auth::SignatureRecovery;
3use hypercall_types::{
4 observability::AuthFailureReason, WalletAddress, API_ROUTE_MARGIN_MODE,
5 API_ROUTE_NOTIFICATIONS_MARK_ALL_READ, API_ROUTE_NOTIFICATIONS_MARK_READ, API_ROUTE_ORDER,
6 API_ROUTE_ORDER_CLOID, API_ROUTE_PROFILE_IMAGE, API_ROUTE_PUSH_PREFERENCES,
7 API_ROUTE_PUSH_SUBSCRIBE, API_ROUTE_PUSH_UNSUBSCRIBE, API_ROUTE_SETTLEMENT_PAYOUTS_SEEN,
8 API_ROUTE_USERNAME,
9};
10use sonic_rs::{JsonContainerTrait, JsonValueTrait};
11use std::str::FromStr;
12
13#[derive(Clone, Debug)]
14pub struct RecoveredSignedAction {
15 pub wallet: WalletAddress,
16 pub signer: WalletAddress,
17 pub nonce: u64,
18}
19
20#[derive(Debug)]
21pub struct SignedActionError {
22 pub status: StatusCode,
23 pub error: &'static str,
24 pub message: String,
25 pub auth_failure_reason: Option<AuthFailureReason>,
26}
27
28impl SignedActionError {
29 fn bad_request(error: &'static str, message: impl Into<String>) -> Self {
30 Self {
31 status: StatusCode::BAD_REQUEST,
32 error,
33 message: message.into(),
34 auth_failure_reason: None,
35 }
36 }
37
38 fn signature(
39 reason: AuthFailureReason,
40 message: impl Into<String>,
41 source: impl std::fmt::Display,
42 ) -> Self {
43 Self::signature_message(reason, format!("{}: {}", message.into(), source))
44 }
45
46 fn signature_message(reason: AuthFailureReason, message: impl Into<String>) -> Self {
47 Self {
48 status: StatusCode::BAD_REQUEST,
49 error: "signature_verification_failed",
50 message: message.into(),
51 auth_failure_reason: Some(reason),
52 }
53 }
54}
55
56fn required_str<'a>(
57 request_data: &'a sonic_rs::Value,
58 field: &'static str,
59) -> Result<&'a str, SignedActionError> {
60 request_data
61 .get(field)
62 .and_then(|v| v.as_str())
63 .ok_or_else(|| {
64 SignedActionError::bad_request(
65 "missing_field",
66 format!("Missing '{field}' field in request"),
67 )
68 })
69}
70
71fn required_u64(
72 request_data: &sonic_rs::Value,
73 field: &'static str,
74) -> Result<u64, SignedActionError> {
75 request_data
76 .get(field)
77 .and_then(|v| v.as_u64())
78 .ok_or_else(|| {
79 SignedActionError::bad_request(
80 "missing_field",
81 format!("Missing '{field}' field in request"),
82 )
83 })
84}
85
86fn required_string_parameter<'a>(
87 request_data: &'a sonic_rs::Value,
88 field: &'static str,
89) -> Result<&'a str, SignedActionError> {
90 request_data
91 .get(field)
92 .and_then(|v| v.as_str())
93 .ok_or_else(|| {
94 SignedActionError::bad_request(
95 "invalid_parameter",
96 format!("{field} must be provided as a string for signature verification"),
97 )
98 })
99}
100
101fn order_id_as_string(request_data: &sonic_rs::Value) -> Result<String, SignedActionError> {
102 if let Some(s) = request_data.get("order_id").and_then(|v| v.as_str()) {
103 Ok(s.to_string())
104 } else if let Some(n) = request_data.get("order_id").and_then(|v| v.as_u64()) {
105 Ok(n.to_string())
106 } else if let Some(n) = request_data.get("order_id").and_then(|v| v.as_i64()) {
107 Ok(n.to_string())
108 } else {
109 Err(SignedActionError::bad_request(
110 "missing_field",
111 "Missing 'order_id' field in request",
112 ))
113 }
114}
115
116fn parse_settlement_payout_ids(
117 request_data: &sonic_rs::Value,
118) -> Result<Vec<i64>, SignedActionError> {
119 let raw_ids = request_data
120 .get("ids")
121 .and_then(|v| v.as_array())
122 .ok_or_else(|| {
123 SignedActionError::bad_request("missing_field", "Missing 'ids' field in request")
124 })?;
125
126 if raw_ids.is_empty() {
127 return Err(SignedActionError::bad_request(
128 "invalid_parameter",
129 "'ids' must not be empty",
130 ));
131 }
132
133 let mut ids = Vec::with_capacity(raw_ids.len());
134 for raw_id in raw_ids {
135 let id = if let Some(v) = raw_id.as_i64() {
136 v
137 } else if let Some(v) = raw_id.as_u64() {
138 if v > i64::MAX as u64 {
139 return Err(SignedActionError::bad_request(
140 "invalid_parameter",
141 "payout id exceeds supported integer range",
142 ));
143 }
144 v as i64
145 } else {
146 return Err(SignedActionError::bad_request(
147 "invalid_parameter",
148 "all ids must be integers",
149 ));
150 };
151
152 if id <= 0 {
153 return Err(SignedActionError::bad_request(
154 "invalid_parameter",
155 "all ids must be positive integers",
156 ));
157 }
158
159 ids.push(id);
160 }
161
162 Ok(ids)
163}
164
165fn canonicalize_settlement_payout_ids(ids: &[i64]) -> String {
166 ids.iter()
167 .map(|id| id.to_string())
168 .collect::<Vec<_>>()
169 .join(",")
170}
171
172pub fn recover_signed_action(
173 path: &str,
174 method: &Method,
175 request_data: &sonic_rs::Value,
176 chain_id: u64,
177) -> Result<RecoveredSignedAction, SignedActionError> {
178 let wallet_str = required_str(request_data, "wallet")?;
179 let wallet = WalletAddress::from_str(wallet_str)
180 .map_err(|_| SignedActionError::bad_request("invalid_wallet", "Invalid wallet address"))?;
181 let nonce = required_u64(request_data, "nonce")?;
182 let signature = required_str(request_data, "signature")?;
183
184 let signer = match path {
185 path if path == API_ROUTE_ORDER && method == Method::POST => {
186 let symbol = required_str(request_data, "symbol")?;
187 let side = required_str(request_data, "side")?;
188 let size = required_string_parameter(request_data, "size")?;
189 let price = required_string_parameter(request_data, "price")?;
190 let tif = request_data
191 .get("tif")
192 .and_then(|v| v.as_str())
193 .unwrap_or("GTC");
194 let client_id = request_data
195 .get("client_id")
196 .and_then(|v| v.as_str())
197 .unwrap_or("");
198
199 SignatureRecovery::recover_place_order_signer(
200 wallet, symbol, side, size, price, tif, client_id, nonce, signature, chain_id,
201 )
202 .map_err(|e| {
203 SignedActionError::signature_message(
204 AuthFailureReason::PlaceOrderSignature,
205 format!(
206 "Signature verification failed: {e}. Ensure size and price strings match exactly what was signed."
207 ),
208 )
209 })?
210 }
211 path if path == API_ROUTE_ORDER && method == Method::DELETE => {
212 let order_id = order_id_as_string(request_data)?;
213 SignatureRecovery::recover_cancel_order_signer(
214 wallet, &order_id, nonce, signature, chain_id,
215 )
216 .map_err(|e| {
217 SignedActionError::signature(
218 AuthFailureReason::CancelOrderSignature,
219 "Signature verification failed",
220 e,
221 )
222 })?
223 }
224 path if path == API_ROUTE_ORDER && method == Method::PUT => {
225 let order_id = order_id_as_string(request_data)?;
226 let symbol = required_str(request_data, "symbol")?;
227 let side = required_str(request_data, "side")?;
228 let size = required_string_parameter(request_data, "size")?;
229 let price = required_string_parameter(request_data, "price")?;
230 let tif = request_data
231 .get("tif")
232 .and_then(|v| v.as_str())
233 .unwrap_or("gtc");
234 let client_id = request_data
235 .get("client_id")
236 .and_then(|v| v.as_str())
237 .unwrap_or("");
238
239 SignatureRecovery::recover_replace_order_signer(
240 wallet, &order_id, symbol, side, size, price, tif, client_id, nonce, signature,
241 chain_id,
242 )
243 .map_err(|e| {
244 SignedActionError::signature_message(
245 AuthFailureReason::PlaceOrderSignature,
246 format!(
247 "Signature verification failed: {e}. Ensure size and price strings match exactly what was signed."
248 ),
249 )
250 })?
251 }
252 path if path == API_ROUTE_ORDER_CLOID && method == Method::DELETE => {
253 let client_id = required_str(request_data, "client_id")?;
254 SignatureRecovery::recover_cancel_order_by_client_id_signer(
255 wallet, client_id, nonce, signature, chain_id,
256 )
257 .map_err(|e| {
258 SignedActionError::signature(
259 AuthFailureReason::CancelOrderCloidSignature,
260 "Signature verification failed",
261 e,
262 )
263 })?
264 }
265 path if path == API_ROUTE_MARGIN_MODE && method == Method::POST => {
266 let margin_mode = required_str(request_data, "margin_mode")?;
267 SignatureRecovery::recover_set_margin_mode_signer(
268 &wallet.to_string(),
269 margin_mode,
270 nonce,
271 signature,
272 chain_id,
273 )
274 .map_err(|e| {
275 SignedActionError::signature(
276 AuthFailureReason::MarginModeSignature,
277 "Signature verification failed",
278 e,
279 )
280 })?
281 }
282 path if path == API_ROUTE_SETTLEMENT_PAYOUTS_SEEN && method == Method::POST => {
283 let ids = parse_settlement_payout_ids(request_data)?;
284 let canonical_ids = canonicalize_settlement_payout_ids(&ids);
285
286 SignatureRecovery::recover_settlement_payout_seen_signer(
287 wallet,
288 &canonical_ids,
289 true,
290 nonce,
291 signature,
292 chain_id,
293 )
294 .map_err(|e| {
295 SignedActionError::signature(
296 AuthFailureReason::SettlementPayoutSeenSignature,
297 "Signature verification failed",
298 e,
299 )
300 })?
301 }
302 path if path == API_ROUTE_USERNAME && method == Method::POST => {
303 let username = required_str(request_data, "username")?;
304 SignatureRecovery::recover_set_username_signer(
305 wallet, username, nonce, signature, chain_id,
306 )
307 .map_err(|e| {
308 SignedActionError::signature(
309 AuthFailureReason::SignatureRecovery,
310 "Signature verification failed",
311 e,
312 )
313 })?
314 }
315 path if path == API_ROUTE_PROFILE_IMAGE && method == Method::POST => {
316 let image_sha256 = required_str(request_data, "image_sha256")?;
317 SignatureRecovery::recover_set_profile_image_signer(
318 wallet,
319 image_sha256,
320 nonce,
321 signature,
322 chain_id,
323 )
324 .map_err(|e| {
325 SignedActionError::signature(
326 AuthFailureReason::SignatureRecovery,
327 "Signature verification failed",
328 e,
329 )
330 })?
331 }
332 path if path == API_ROUTE_USERNAME && method == Method::DELETE => {
333 SignatureRecovery::recover_delete_username_signer(wallet, nonce, signature, chain_id)
334 .map_err(|e| {
335 SignedActionError::signature(
336 AuthFailureReason::SignatureRecovery,
337 "Signature verification failed",
338 e,
339 )
340 })?
341 }
342 path if matches!(
343 path,
344 API_ROUTE_PUSH_SUBSCRIBE
345 | API_ROUTE_PUSH_UNSUBSCRIBE
346 | API_ROUTE_PUSH_PREFERENCES
347 | API_ROUTE_NOTIFICATIONS_MARK_READ
348 | API_ROUTE_NOTIFICATIONS_MARK_ALL_READ
349 ) && method == Method::POST =>
350 {
351 let action = required_str(request_data, "action")?;
352 SignatureRecovery::recover_push_action_signer(
353 wallet, action, nonce, signature, chain_id,
354 )
355 .map_err(|e| {
356 SignedActionError::signature(
357 AuthFailureReason::SignatureRecovery,
358 "Signature verification failed",
359 e,
360 )
361 })?
362 }
363 "/withdraw/option" if method == Method::POST => {
364 let account = required_str(request_data, "account")?;
365 let symbol = required_str(request_data, "symbol")?;
366 let amount = required_str(request_data, "amount")?;
367 let account_addr = account.parse::<WalletAddress>().map_err(|_| {
368 SignedActionError::bad_request("bad_request", "invalid account address")
369 })?;
370 SignatureRecovery::recover_withdraw_option_signer(
371 wallet,
372 account_addr,
373 symbol,
374 amount,
375 nonce,
376 signature,
377 chain_id,
378 )
379 .map_err(|e| {
380 SignedActionError::signature(
381 AuthFailureReason::SignatureRecovery,
382 "Signature verification failed",
383 e,
384 )
385 })?
386 }
387 "/withdraw/usdc" if method == Method::POST => {
388 let account = required_str(request_data, "account")?;
389 let destination = required_str(request_data, "destination")?;
390 let amount = required_str(request_data, "amount")?;
391 let account_addr = account.parse::<WalletAddress>().map_err(|_| {
392 SignedActionError::bad_request("bad_request", "invalid account address")
393 })?;
394 let destination_addr = destination.parse::<WalletAddress>().map_err(|_| {
395 SignedActionError::bad_request("bad_request", "invalid destination address")
396 })?;
397 SignatureRecovery::recover_withdraw_usdc_signer(
398 wallet,
399 account_addr,
400 destination_addr,
401 amount,
402 nonce,
403 signature,
404 chain_id,
405 )
406 .map_err(|e| {
407 SignedActionError::signature(
408 AuthFailureReason::SignatureRecovery,
409 "Signature verification failed",
410 e,
411 )
412 })?
413 }
414 _ => {
415 return Err(SignedActionError::bad_request(
416 "unsupported_endpoint",
417 format!("Unsupported endpoint for signature middleware: {path}"),
418 ));
419 }
420 };
421
422 Ok(RecoveredSignedAction {
423 wallet,
424 signer: WalletAddress::from(signer),
425 nonce,
426 })
427}