1use std::collections::HashMap;
2
3use alloy::primitives::Address;
4use polyoxide_core::{HttpClient, QueryBuilder};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 account::{Credentials, Signer, Wallet},
9 error::ClobError,
10 request::{AuthMode, Request},
11 types::{OrderSide, SignatureType},
12};
13
14#[derive(Clone)]
16pub struct AccountApi {
17 pub(crate) http_client: HttpClient,
18 pub(crate) wallet: Wallet,
19 pub(crate) credentials: Credentials,
20 pub(crate) signer: Signer,
21 pub(crate) chain_id: u64,
22 pub(crate) signature_type: SignatureType,
23}
24
25impl AccountApi {
26 pub fn balance_allowance(
28 &self,
29 token_id: impl Into<String>,
30 ) -> Request<BalanceAllowanceResponse> {
31 Request::get(
32 self.http_client.clone(),
33 "/balance-allowance",
34 AuthMode::L2 {
35 address: self.wallet.clone().address(),
36 credentials: self.credentials.clone(),
37 signer: self.signer.clone(),
38 },
39 self.chain_id,
40 )
41 .query("asset_type", "CONDITIONAL")
42 .query("token_id", token_id.into())
43 .query("signature_type", self.signature_type as u8)
44 }
45
46 pub fn usdc_balance(&self) -> Request<BalanceAllowanceResponse> {
48 Request::get(
49 self.http_client.clone(),
50 "/balance-allowance",
51 AuthMode::L2 {
52 address: self.wallet.clone().address(),
53 credentials: self.credentials.clone(),
54 signer: self.signer.clone(),
55 },
56 self.chain_id,
57 )
58 .query("asset_type", "COLLATERAL")
59 .query("signature_type", self.signature_type as u8)
60 }
61
62 pub async fn update_balance_allowance(
72 &self,
73 asset_type: impl Into<String>,
74 token_id: Option<String>,
75 signature_type: Option<u8>,
76 ) -> Result<serde_json::Value, ClobError> {
77 let mut request = Request::<serde_json::Value>::get(
78 self.http_client.clone(),
79 "/balance-allowance/update".to_string(),
80 AuthMode::L2 {
81 address: self.wallet.clone().address(),
82 credentials: self.credentials.clone(),
83 signer: self.signer.clone(),
84 },
85 self.chain_id,
86 )
87 .query("asset_type", asset_type.into());
88 if let Some(token_id) = token_id {
89 request = request.query("token_id", token_id);
90 }
91 if let Some(signature_type) = signature_type {
92 request = request.query("signature_type", signature_type);
93 }
94
95 let text = request.send_raw().await?.text().await?;
99 if text.trim().is_empty() {
100 return Ok(serde_json::Value::Null);
101 }
102 Ok(serde_json::from_str(&text)?)
103 }
104
105 pub async fn heartbeat(&self) -> Result<HeartbeatResponse, ClobError> {
111 Request::<HeartbeatResponse>::post(
112 self.http_client.clone(),
113 "/heartbeats".to_string(),
114 AuthMode::L2 {
115 address: self.wallet.clone().address(),
116 credentials: self.credentials.clone(),
117 signer: self.signer.clone(),
118 },
119 self.chain_id,
120 )
121 .send()
122 .await
123 }
124
125 pub async fn heartbeat_v1(&self) -> Result<serde_json::Value, ClobError> {
129 Request::<serde_json::Value>::post(
130 self.http_client.clone(),
131 "/v1/heartbeats".to_string(),
132 AuthMode::L2 {
133 address: self.wallet.clone().address(),
134 credentials: self.credentials.clone(),
135 signer: self.signer.clone(),
136 },
137 self.chain_id,
138 )
139 .send()
140 .await
141 }
142
143 pub fn builder_trades(&self, builder_code: impl Into<String>) -> ListBuilderTrades {
149 let request = Request::get(
150 self.http_client.clone(),
151 "/builder/trades",
152 AuthMode::L2 {
153 address: self.wallet.clone().address(),
154 credentials: self.credentials.clone(),
155 signer: self.signer.clone(),
156 },
157 self.chain_id,
158 )
159 .query("builder_code", builder_code.into());
160 ListBuilderTrades { request }
161 }
162
163 pub fn trades(&self, maker_address: impl Into<String>) -> ListClobTrades {
165 let request = Request::get(
166 self.http_client.clone(),
167 "/data/trades",
168 AuthMode::L2 {
169 address: self.wallet.clone().address(),
170 credentials: self.credentials.clone(),
171 signer: self.signer.clone(),
172 },
173 self.chain_id,
174 )
175 .query("maker_address", maker_address.into());
176 ListClobTrades { request }
177 }
178}
179
180pub struct ListClobTrades {
182 request: Request<ListTradesResponse>,
183}
184
185impl ListClobTrades {
186 pub fn id(mut self, id: impl Into<String>) -> Self {
188 self.request = self.request.query("id", id.into());
189 self
190 }
191
192 pub fn market(mut self, condition_id: impl Into<String>) -> Self {
194 self.request = self.request.query("market", condition_id.into());
195 self
196 }
197
198 pub fn asset_id(mut self, token_id: impl Into<String>) -> Self {
200 self.request = self.request.query("asset_id", token_id.into());
201 self
202 }
203
204 pub fn before(mut self, timestamp: impl Into<String>) -> Self {
206 self.request = self.request.query("before", timestamp.into());
207 self
208 }
209
210 pub fn after(mut self, timestamp: impl Into<String>) -> Self {
212 self.request = self.request.query("after", timestamp.into());
213 self
214 }
215
216 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
218 self.request = self.request.query("next_cursor", cursor.into());
219 self
220 }
221
222 pub async fn send(self) -> Result<ListTradesResponse, ClobError> {
224 self.request.send().await
225 }
226}
227
228pub struct ListBuilderTrades {
230 request: Request<ListBuilderTradesResponse>,
231}
232
233impl ListBuilderTrades {
234 pub fn after(mut self, cursor: impl Into<String>) -> Self {
236 self.request = self.request.query("after", cursor.into());
237 self
238 }
239
240 pub fn maker_address(mut self, address: impl Into<String>) -> Self {
242 self.request = self.request.query("maker_address", address.into());
243 self
244 }
245
246 pub fn market(mut self, condition_id: impl Into<String>) -> Self {
248 self.request = self.request.query("market", condition_id.into());
249 self
250 }
251
252 pub fn id(mut self, id: impl Into<String>) -> Self {
254 self.request = self.request.query("id", id.into());
255 self
256 }
257
258 pub fn asset_id(mut self, token_id: impl Into<String>) -> Self {
260 self.request = self.request.query("asset_id", token_id.into());
261 self
262 }
263
264 pub fn before(mut self, timestamp: impl Into<String>) -> Self {
266 self.request = self.request.query("before", timestamp.into());
267 self
268 }
269
270 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
272 self.request = self.request.query("next_cursor", cursor.into());
273 self
274 }
275
276 pub async fn send(self) -> Result<ListBuilderTradesResponse, ClobError> {
278 self.request.send().await
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct Trade {
285 pub id: String,
286 pub taker_order_id: String,
287 pub market: String,
288 pub asset_id: String,
289 pub side: OrderSide,
290 pub size: String,
291 pub fee_rate_bps: String,
292 pub price: String,
293 pub status: String,
294 pub match_time: String,
295 #[serde(default)]
296 pub last_update: Option<String>,
297 pub outcome: String,
298 #[serde(default)]
299 pub bucket_index: Option<u32>,
300 pub owner: Address,
301 pub maker_address: Option<String>,
302 #[serde(default)]
303 pub maker_orders: Vec<MakerOrder>,
304 pub transaction_hash: String,
305 pub trader_side: Option<String>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct MakerOrder {
311 pub order_id: String,
312 pub owner: String,
313 pub maker_address: String,
314 pub matched_amount: String,
315 pub price: String,
316 pub fee_rate_bps: String,
317 pub asset_id: String,
318 pub outcome: String,
319 pub side: OrderSide,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct ListTradesResponse {
325 pub data: Vec<Trade>,
326 pub next_cursor: Option<String>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct BuilderTrade {
336 pub id: String,
337 pub trade_type: String,
338 pub taker_order_hash: String,
339 pub builder: String,
340 pub market: String,
341 pub asset_id: String,
342 pub side: String,
343 pub size: String,
344 pub size_usdc: String,
345 pub price: String,
346 pub status: String,
347 pub outcome: String,
348 pub outcome_index: u32,
349 pub owner: String,
350 pub maker: String,
351 pub transaction_hash: String,
352 pub match_time: String,
353 #[serde(default)]
354 pub bucket_index: Option<u32>,
355 pub fee: String,
356 pub fee_usdc: String,
357 #[serde(rename = "err_msg")]
358 pub err_msg: Option<String>,
359 pub created_at: Option<String>,
360 pub updated_at: Option<String>,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct ListBuilderTradesResponse {
366 pub data: Vec<BuilderTrade>,
367 pub next_cursor: Option<String>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct BalanceAllowanceResponse {
373 pub balance: String,
374 pub allowances: HashMap<String, String>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct HeartbeatResponse {
380 pub status: String,
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn trade_deserialization() {
389 let json = r#"{
390 "id": "trade-123",
391 "taker_order_id": "order-456",
392 "market": "0xcondition",
393 "asset_id": "0xtoken",
394 "side": "BUY",
395 "size": "100.5",
396 "fee_rate_bps": "0",
397 "price": "0.55",
398 "status": "MATCHED",
399 "match_time": "1700000000",
400 "last_update": null,
401 "outcome": "Yes",
402 "bucket_index": null,
403 "owner": "0x0000000000000000000000000000000000000001",
404 "transaction_hash": "0xhash123"
405 }"#;
406 let trade: Trade = serde_json::from_str(json).unwrap();
407 assert_eq!(trade.id, "trade-123");
408 assert_eq!(trade.side, OrderSide::Buy);
409 assert_eq!(trade.price, "0.55");
410 assert!(trade.last_update.is_none());
411 assert!(trade.bucket_index.is_none());
412 assert!(trade.maker_address.is_none());
414 assert!(trade.maker_orders.is_empty());
415 assert!(trade.trader_side.is_none());
416 }
417
418 #[test]
419 fn trade_with_optional_fields() {
420 let json = r#"{
421 "id": "t1",
422 "taker_order_id": "o1",
423 "market": "0xcond",
424 "asset_id": "0xasset",
425 "side": "SELL",
426 "size": "50",
427 "fee_rate_bps": "100",
428 "price": "0.72",
429 "status": "MATCHED",
430 "match_time": "1700001000",
431 "last_update": "1700002000",
432 "outcome": "No",
433 "bucket_index": 3,
434 "owner": "0x0000000000000000000000000000000000000002",
435 "maker_address": "0xmaker",
436 "maker_orders": [{
437 "order_id": "mo-1",
438 "owner": "0xowner",
439 "maker_address": "0xmaker",
440 "matched_amount": "50",
441 "price": "0.72",
442 "fee_rate_bps": "100",
443 "asset_id": "0xasset",
444 "outcome": "No",
445 "side": "BUY"
446 }],
447 "transaction_hash": "0xhash456",
448 "trader_side": "TAKER"
449 }"#;
450 let trade: Trade = serde_json::from_str(json).unwrap();
451 assert_eq!(trade.side, OrderSide::Sell);
452 assert_eq!(trade.last_update.as_deref(), Some("1700002000"));
453 assert_eq!(trade.bucket_index, Some(3));
454 assert_eq!(trade.maker_address.as_deref(), Some("0xmaker"));
455 assert_eq!(trade.maker_orders.len(), 1);
456 assert_eq!(trade.maker_orders[0].order_id, "mo-1");
457 assert_eq!(trade.maker_orders[0].matched_amount, "50");
458 assert_eq!(trade.maker_orders[0].side, OrderSide::Buy);
459 assert_eq!(trade.trader_side.as_deref(), Some("TAKER"));
460 }
461
462 #[test]
463 fn list_trades_response_deserializes() {
464 let json = r#"{
465 "data": [{
466 "id": "t1",
467 "taker_order_id": "o1",
468 "market": "0xcond",
469 "asset_id": "0xasset",
470 "side": "BUY",
471 "size": "100",
472 "fee_rate_bps": "0",
473 "price": "0.55",
474 "status": "MATCHED",
475 "match_time": "1700000000",
476 "outcome": "Yes",
477 "owner": "0x0000000000000000000000000000000000000001",
478 "transaction_hash": "0xhash"
479 }],
480 "next_cursor": "abc123"
481 }"#;
482 let resp: ListTradesResponse = serde_json::from_str(json).unwrap();
483 assert_eq!(resp.data.len(), 1);
484 assert_eq!(resp.data[0].id, "t1");
485 assert_eq!(resp.next_cursor.as_deref(), Some("abc123"));
486 }
487
488 #[test]
489 fn list_trades_response_empty() {
490 let json = r#"{"data": [], "next_cursor": null}"#;
491 let resp: ListTradesResponse = serde_json::from_str(json).unwrap();
492 assert!(resp.data.is_empty());
493 assert!(resp.next_cursor.is_none());
494 }
495
496 #[test]
497 fn builder_trade_deserialization() {
498 let json = r#"{
499 "id": "bt-1",
500 "tradeType": "LIMIT",
501 "takerOrderHash": "0xhash",
502 "builder": "0xbuilder",
503 "market": "0xcond",
504 "assetId": "0xtoken",
505 "side": "BUY",
506 "size": "100",
507 "sizeUsdc": "55.00",
508 "price": "0.55",
509 "status": "MATCHED",
510 "outcome": "Yes",
511 "outcomeIndex": 0,
512 "owner": "0xowner",
513 "maker": "0xmaker",
514 "transactionHash": "0xtxhash",
515 "matchTime": "1700000000",
516 "bucketIndex": 5,
517 "fee": "0.01",
518 "feeUsdc": "0.55",
519 "err_msg": null,
520 "createdAt": "2024-01-01T00:00:00Z",
521 "updatedAt": "2024-01-01T00:00:01Z"
522 }"#;
523 let bt: BuilderTrade = serde_json::from_str(json).unwrap();
524 assert_eq!(bt.id, "bt-1");
525 assert_eq!(bt.trade_type, "LIMIT");
526 assert_eq!(bt.builder, "0xbuilder");
527 assert_eq!(bt.size_usdc, "55.00");
528 assert_eq!(bt.outcome_index, 0);
529 assert_eq!(bt.bucket_index, Some(5));
530 assert!(bt.err_msg.is_none());
531 assert_eq!(bt.created_at.as_deref(), Some("2024-01-01T00:00:00Z"));
532 }
533
534 #[test]
535 fn builder_trade_with_error() {
536 let json = r#"{
537 "id": "bt-2",
538 "tradeType": "MARKET",
539 "takerOrderHash": "0x",
540 "builder": "0x",
541 "market": "0x",
542 "assetId": "0x",
543 "side": "SELL",
544 "size": "0",
545 "sizeUsdc": "0",
546 "price": "0",
547 "status": "FAILED",
548 "outcome": "No",
549 "outcomeIndex": 1,
550 "owner": "0x",
551 "maker": "0x",
552 "transactionHash": "0x",
553 "matchTime": "0",
554 "fee": "0",
555 "feeUsdc": "0",
556 "err_msg": "insufficient balance",
557 "createdAt": null,
558 "updatedAt": null
559 }"#;
560 let bt: BuilderTrade = serde_json::from_str(json).unwrap();
561 assert_eq!(bt.err_msg.as_deref(), Some("insufficient balance"));
562 assert!(bt.bucket_index.is_none());
563 assert!(bt.created_at.is_none());
564 }
565
566 #[test]
567 fn list_builder_trades_response_deserializes() {
568 let json = r#"{
569 "data": [{
570 "id": "bt-1",
571 "tradeType": "LIMIT",
572 "takerOrderHash": "0x",
573 "builder": "0x",
574 "market": "0x",
575 "assetId": "0x",
576 "side": "BUY",
577 "size": "100",
578 "sizeUsdc": "55",
579 "price": "0.55",
580 "status": "MATCHED",
581 "outcome": "Yes",
582 "outcomeIndex": 0,
583 "owner": "0x",
584 "maker": "0x",
585 "transactionHash": "0x",
586 "matchTime": "0",
587 "fee": "0",
588 "feeUsdc": "0",
589 "createdAt": null,
590 "updatedAt": null
591 }],
592 "next_cursor": "cursor123"
593 }"#;
594 let resp: ListBuilderTradesResponse = serde_json::from_str(json).unwrap();
595 assert_eq!(resp.data.len(), 1);
596 assert_eq!(resp.data[0].id, "bt-1");
597 assert_eq!(resp.next_cursor.as_deref(), Some("cursor123"));
598 }
599
600 #[test]
601 fn balance_allowance_response_deserializes() {
602 let json = r#"{"balance": "141171137", "allowances": {"0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E": "999999"}}"#;
603 let resp: BalanceAllowanceResponse = serde_json::from_str(json).unwrap();
604 assert_eq!(resp.balance, "141171137");
605 assert_eq!(
606 resp.allowances
607 .get("0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E")
608 .unwrap(),
609 "999999"
610 );
611 }
612}