1use crate::error::ApiError;
2use crate::request_auth::verify_request;
3pub use crate::rfq::handler_state::RfqHandlerState;
4use crate::rfq::rfq_manager::{RfqLeg, RfqManager};
5use crate::sonic_json::SonicJson;
6use axum::extract::{Path, State};
7use hypercall_auth::SignedAcceptRfqQuote;
8use hypercall_runtime_api::RfqExecuteResult;
9use hypercall_types::{
10 AcceptRfqRequest, RfqAcceptResponse, RfqHistoryResponse, RfqLegResponse, RfqQuoteLegResponse,
11 RfqQuoteResponse, RfqStatus, RfqStatusResponse, SubmitRfqRequest, WalletAddress,
12};
13use rust_decimal::Decimal;
14use std::str::FromStr;
15use tracing::info_span;
16use tracing::Instrument;
17use uuid::Uuid;
18
19pub async fn submit_rfq_inner(
24 rfq_manager: &RfqManager,
25 agent_auth: &dyn hypercall_runtime_api::AgentAuthProvider,
26 rfq_id_str: &str,
27 legs: &[hypercall_types::RfqLegRequest],
28 wallet_address: WalletAddress,
29 nonce: u64,
30 signature: &str,
31 chain_id: u64,
32) -> Result<crate::rfq::rfq_manager::RfqRecord, String> {
33 let rfq_id = Uuid::parse_str(rfq_id_str).map_err(|_| "Invalid rfq_id format".to_string())?;
34
35 if legs.is_empty() || legs.len() > 10 {
36 return Err("RFQ must have 1-10 legs".to_string());
37 }
38
39 let parsed_legs: Vec<RfqLeg> = legs
45 .iter()
46 .map(|l| {
47 let size = l
48 .size
49 .parse::<Decimal>()
50 .map_err(|_| "Invalid leg size".to_string())?;
51 if size <= Decimal::ZERO {
52 return Err(format!(
53 "Leg size must be strictly positive, got {} for instrument {}",
54 size, l.instrument
55 ));
56 }
57 Ok(RfqLeg {
58 instrument: l.instrument.clone(),
59 side: l.side,
60 size,
61 })
62 })
63 .collect::<Result<Vec<_>, String>>()?;
64
65 let legs_hash = compute_legs_hash_from_legs(legs)?;
67
68 let verification_request = SubmitRfqRequest {
69 rfq_id: rfq_id_str.to_string(),
70 legs: legs.to_vec(),
71 wallet_address,
72 nonce,
73 signature: signature.to_string(),
74 auto_accept_limit: None,
75 };
76 let authorized =
77 verify_request(agent_auth, &verification_request, chain_id).map_err(|e| e.to_string())?;
78
79 rfq_manager
80 .submit_rfq(
81 rfq_id,
82 wallet_address,
83 authorized.signer.signer_address,
84 parsed_legs,
85 legs_hash,
86 signature.to_string(),
87 nonce,
88 None,
89 )
90 .await
91 .map_err(|e| e.to_string())
92}
93
94#[allow(clippy::too_many_arguments)]
99pub async fn submit_auto_execute_rfq_inner(
100 rfq_manager: &RfqManager,
101 agent_auth: &dyn hypercall_runtime_api::AgentAuthProvider,
102 rfq_id_str: &str,
103 legs: &[hypercall_types::RfqLegRequest],
104 wallet_address: WalletAddress,
105 limit_price_str: &str,
106 nonce: u64,
107 signature: &str,
108 chain_id: u64,
109) -> Result<crate::rfq::rfq_manager::RfqRecord, String> {
110 let rfq_id = Uuid::parse_str(rfq_id_str).map_err(|_| "Invalid rfq_id format".to_string())?;
111
112 if legs.is_empty() || legs.len() > 10 {
113 return Err("RFQ must have 1-10 legs".to_string());
114 }
115
116 let limit_price = limit_price_str
117 .parse::<Decimal>()
118 .map_err(|_| "Invalid limit_price format".to_string())?;
119 if limit_price <= Decimal::ZERO {
120 return Err("limit_price must be positive".to_string());
121 }
122
123 let parsed_legs: Vec<RfqLeg> = legs
125 .iter()
126 .map(|l| {
127 let size = l
128 .size
129 .parse::<Decimal>()
130 .map_err(|_| "Invalid leg size".to_string())?;
131 if size <= Decimal::ZERO {
132 return Err(format!(
133 "Leg size must be strictly positive, got {} for instrument {}",
134 size, l.instrument
135 ));
136 }
137 Ok(RfqLeg {
138 instrument: l.instrument.clone(),
139 side: l.side,
140 size,
141 })
142 })
143 .collect::<Result<Vec<_>, String>>()?;
144
145 let legs_hash = compute_legs_hash_from_legs(legs)?;
146
147 let verification_request = SubmitRfqRequest {
148 rfq_id: rfq_id_str.to_string(),
149 legs: legs.to_vec(),
150 wallet_address,
151 nonce,
152 signature: signature.to_string(),
153 auto_accept_limit: Some(limit_price_str.to_string()),
154 };
155 let authorized =
156 verify_request(agent_auth, &verification_request, chain_id).map_err(|e| e.to_string())?;
157
158 rfq_manager
159 .submit_rfq(
160 rfq_id,
161 wallet_address,
162 authorized.signer.signer_address,
163 parsed_legs,
164 legs_hash,
165 signature.to_string(),
166 nonce,
167 Some(limit_price),
168 )
169 .await
170 .map_err(|e| e.to_string())
171}
172
173pub async fn submit_rfq(
175 State(state): State<RfqHandlerState>,
176 SonicJson(request): SonicJson<SubmitRfqRequest>,
177) -> Result<SonicJson<RfqStatusResponse>, ApiError> {
178 let span = info_span!("api.submit_rfq", wallet = %request.wallet_address);
179 async move {
180 if let Some(ref limit_str) = request.auto_accept_limit {
182 let record = submit_auto_execute_rfq_inner(
183 &state.rfq_manager,
184 state.agent_auth.as_ref(),
185 &request.rfq_id,
186 &request.legs,
187 request.wallet_address,
188 limit_str,
189 request.nonce,
190 &request.signature,
191 state.signing_chain_id,
192 )
193 .await
194 .map_err(|e| ApiError::bad_request(&e))?;
195 return Ok(SonicJson(rfq_record_to_response(&record)));
196 }
197
198 let record = submit_rfq_inner(
199 &state.rfq_manager,
200 state.agent_auth.as_ref(),
201 &request.rfq_id,
202 &request.legs,
203 request.wallet_address,
204 request.nonce,
205 &request.signature,
206 state.signing_chain_id,
207 )
208 .await
209 .map_err(|e| ApiError::bad_request(&e))?;
210
211 Ok(SonicJson(rfq_record_to_response(&record)))
212 }
213 .instrument(span)
214 .await
215}
216
217pub async fn accept_rfq_quote_inner(
222 rfq_manager: &RfqManager,
223 agent_auth: &dyn hypercall_runtime_api::AgentAuthProvider,
224 rfq_id_str: &str,
225 quote_id_str: &str,
226 wallet_address: WalletAddress,
227 nonce: u64,
228 signature: &str,
229 chain_id: u64,
230) -> Result<RfqAcceptResponse, String> {
231 let rfq_id = Uuid::parse_str(rfq_id_str).map_err(|_| "Invalid rfq_id format".to_string())?;
232 let quote_id =
233 Uuid::parse_str(quote_id_str).map_err(|_| "Invalid quote_id format".to_string())?;
234
235 let rfq_record = rfq_manager
237 .get_rfq(&rfq_id)
238 .ok_or_else(|| "RFQ not found".to_string())?;
239 let quote = rfq_record
240 .quotes
241 .iter()
242 .find(|q| q.quote_id == quote_id)
243 .ok_or_else(|| "Quote not found".to_string())?;
244
245 use rust_decimal::prelude::ToPrimitive;
246 let premium_micro = (quote.net_premium * Decimal::new(1_000_000, 0))
247 .to_i64()
248 .ok_or_else(|| "Net premium overflow".to_string())?;
249 let net_premium_i256 = alloy::primitives::I256::try_from(premium_micro)
250 .map_err(|e| format!("I256 conversion: {}", e))?;
251
252 let verification_request = AcceptRfqRequest {
253 rfq_id: rfq_id_str.to_string(),
254 quote_id: quote_id_str.to_string(),
255 wallet_address,
256 nonce,
257 signature: signature.to_string(),
258 };
259 let authorized = verify_request(
260 agent_auth,
261 &SignedAcceptRfqQuote {
262 request: &verification_request,
263 net_premium: net_premium_i256,
264 },
265 chain_id,
266 )
267 .map_err(|e| e.to_string())?;
268
269 let result = rfq_manager
270 .accept_quote(
271 rfq_id,
272 quote_id,
273 wallet_address,
274 signature.to_string(),
275 authorized.signer.signer_address,
276 authorized.nonce,
277 )
278 .await
279 .map_err(|e| e.to_string())?;
280
281 match result {
282 RfqExecuteResult::Success { fill_id } => Ok(RfqAcceptResponse {
283 rfq_id: rfq_id_str.to_string(),
284 quote_id: quote_id_str.to_string(),
285 status: RfqStatus::Executed,
286 fill_id,
287 }),
288 RfqExecuteResult::Failed { reason } => Err(format!("RFQ execution failed: {}", reason)),
289 }
290}
291
292pub async fn accept_rfq_quote(
294 State(state): State<RfqHandlerState>,
295 SonicJson(request): SonicJson<AcceptRfqRequest>,
296) -> Result<SonicJson<RfqAcceptResponse>, ApiError> {
297 let span = info_span!("api.accept_rfq_quote", wallet = %request.wallet_address);
298 async move {
299 let response = accept_rfq_quote_inner(
300 &state.rfq_manager,
301 state.agent_auth.as_ref(),
302 &request.rfq_id,
303 &request.quote_id,
304 request.wallet_address,
305 request.nonce,
306 &request.signature,
307 state.signing_chain_id,
308 )
309 .await
310 .map_err(|e| ApiError::bad_request(&e))?;
311
312 Ok(SonicJson(response))
313 }
314 .instrument(span)
315 .await
316}
317
318pub async fn get_rfq(
320 State(state): State<RfqHandlerState>,
321 Path(rfq_id): Path<String>,
322) -> Result<SonicJson<RfqStatusResponse>, ApiError> {
323 let rfq_uuid =
324 Uuid::parse_str(&rfq_id).map_err(|_| ApiError::bad_request("Invalid rfq_id format"))?;
325
326 let record = state
327 .rfq_manager
328 .get_rfq(&rfq_uuid)
329 .ok_or_else(|| ApiError::not_found("RFQ not found"))?;
330
331 Ok(SonicJson(rfq_record_to_response(&record)))
332}
333
334pub async fn get_rfq_history(
336 State(state): State<RfqHandlerState>,
337 axum::extract::Query(params): axum::extract::Query<RfqHistoryQuery>,
338) -> Result<SonicJson<RfqHistoryResponse>, ApiError> {
339 let wallet = WalletAddress::from_str(¶ms.wallet)
340 .map_err(|_| ApiError::bad_request("Invalid wallet address"))?;
341
342 let records = state.rfq_manager.get_history(&wallet);
343 let rfqs = records.iter().map(rfq_record_to_response).collect();
344
345 Ok(SonicJson(RfqHistoryResponse { rfqs }))
346}
347
348#[derive(serde::Deserialize)]
349pub struct RfqHistoryQuery {
350 pub wallet: String,
351}
352
353pub(crate) fn compute_legs_hash_from_legs(
367 legs: &[hypercall_types::RfqLegRequest],
368) -> Result<[u8; 32], String> {
369 hypercall_auth::compute_rfq_legs_hash(legs)
370}
371
372fn rfq_record_to_response(record: &crate::rfq::rfq_manager::RfqRecord) -> RfqStatusResponse {
373 RfqStatusResponse {
374 rfq_id: record.rfq_id.to_string(),
375 status: record.status,
376 underlying: record.underlying.clone(),
377 legs: record
378 .legs
379 .iter()
380 .map(|l| RfqLegResponse {
381 instrument: l.instrument.clone(),
382 side: l.side,
383 size: l.size,
384 })
385 .collect(),
386 quotes: record
387 .quotes
388 .iter()
389 .map(|q| RfqQuoteResponse {
390 quote_id: q.quote_id.to_string(),
391 net_premium: q.net_premium,
392 legs: q
393 .legs
394 .iter()
395 .map(|l| RfqQuoteLegResponse {
396 instrument: l.instrument.clone(),
397 side: l.side,
398 price: l.price,
399 size: l.size,
400 })
401 .collect(),
402 expires_at: q.received_at + q.valid_for_ms,
403 })
404 .collect(),
405 created_at: record.created_at,
406 expires_at: record.expires_at,
407 }
408}