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,
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}
23
24impl AccountApi {
25 pub fn balance_allowance(
27 &self,
28 token_id: impl Into<String>,
29 ) -> Request<BalanceAllowanceResponse> {
30 Request::get(
31 self.http_client.clone(),
32 "/balance-allowance",
33 AuthMode::L2 {
34 address: self.wallet.clone().address(),
35 credentials: self.credentials.clone(),
36 signer: self.signer.clone(),
37 },
38 self.chain_id,
39 )
40 .query("token_id", token_id.into())
41 }
42
43 pub fn usdc_balance(&self) -> Request<BalanceAllowanceResponse> {
44 Request::get(
45 self.http_client.clone(),
46 "/balance-allowance",
47 AuthMode::L2 {
48 address: self.wallet.clone().address(),
49 credentials: self.credentials.clone(),
50 signer: self.signer.clone(),
51 },
52 self.chain_id,
53 )
54 .query("asset_type", "COLLATERAL")
55 .query("signature_type", 1)
56 }
57
58 pub async fn update_balance_allowance(
60 &self,
61 token_id: impl Into<String>,
62 ) -> Result<serde_json::Value, ClobError> {
63 #[derive(Serialize)]
64 struct Body {
65 token_id: String,
66 }
67
68 Request::<serde_json::Value>::post(
69 self.http_client.clone(),
70 "/balance-allowance/update".to_string(),
71 AuthMode::L2 {
72 address: self.wallet.clone().address(),
73 credentials: self.credentials.clone(),
74 signer: self.signer.clone(),
75 },
76 self.chain_id,
77 )
78 .body(&Body {
79 token_id: token_id.into(),
80 })?
81 .send()
82 .await
83 }
84
85 pub async fn heartbeat(&self) -> Result<serde_json::Value, ClobError> {
87 Request::<serde_json::Value>::post(
88 self.http_client.clone(),
89 "/v1/heartbeats".to_string(),
90 AuthMode::L2 {
91 address: self.wallet.clone().address(),
92 credentials: self.credentials.clone(),
93 signer: self.signer.clone(),
94 },
95 self.chain_id,
96 )
97 .send()
98 .await
99 }
100
101 pub fn builder_trades(&self) -> ListBuilderTrades {
103 let request = Request::get(
104 self.http_client.clone(),
105 "/builder/trades",
106 AuthMode::L2 {
107 address: self.wallet.clone().address(),
108 credentials: self.credentials.clone(),
109 signer: self.signer.clone(),
110 },
111 self.chain_id,
112 );
113 ListBuilderTrades { request }
114 }
115
116 pub fn trades(&self) -> ListClobTrades {
118 let request = Request::get(
119 self.http_client.clone(),
120 "/data/trades",
121 AuthMode::L2 {
122 address: self.wallet.clone().address(),
123 credentials: self.credentials.clone(),
124 signer: self.signer.clone(),
125 },
126 self.chain_id,
127 );
128 ListClobTrades { request }
129 }
130}
131
132pub struct ListClobTrades {
134 request: Request<ListTradesResponse>,
135}
136
137impl ListClobTrades {
138 pub fn id(mut self, id: impl Into<String>) -> Self {
140 self.request = self.request.query("id", id.into());
141 self
142 }
143
144 pub fn maker_address(mut self, address: impl Into<String>) -> Self {
146 self.request = self.request.query("maker_address", address.into());
147 self
148 }
149
150 pub fn market(mut self, condition_id: impl Into<String>) -> Self {
152 self.request = self.request.query("market", condition_id.into());
153 self
154 }
155
156 pub fn asset_id(mut self, token_id: impl Into<String>) -> Self {
158 self.request = self.request.query("asset_id", token_id.into());
159 self
160 }
161
162 pub fn before(mut self, timestamp: impl Into<String>) -> Self {
164 self.request = self.request.query("before", timestamp.into());
165 self
166 }
167
168 pub fn after(mut self, timestamp: impl Into<String>) -> Self {
170 self.request = self.request.query("after", timestamp.into());
171 self
172 }
173
174 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
176 self.request = self.request.query("next_cursor", cursor.into());
177 self
178 }
179
180 pub async fn send(self) -> Result<ListTradesResponse, ClobError> {
182 self.request.send().await
183 }
184}
185
186pub struct ListBuilderTrades {
188 request: Request<ListBuilderTradesResponse>,
189}
190
191impl ListBuilderTrades {
192 pub fn after(mut self, cursor: impl Into<String>) -> Self {
194 self.request = self.request.query("after", cursor.into());
195 self
196 }
197
198 pub fn maker_address(mut self, address: impl Into<String>) -> Self {
200 self.request = self.request.query("maker_address", address.into());
201 self
202 }
203
204 pub fn market(mut self, condition_id: impl Into<String>) -> Self {
206 self.request = self.request.query("market", condition_id.into());
207 self
208 }
209
210 pub async fn send(self) -> Result<ListBuilderTradesResponse, ClobError> {
212 self.request.send().await
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Trade {
219 pub id: String,
220 pub taker_order_id: String,
221 pub market: String,
222 pub asset_id: String,
223 pub side: OrderSide,
224 pub size: String,
225 pub fee_rate_bps: String,
226 pub price: String,
227 pub status: String,
228 pub match_time: String,
229 #[serde(default)]
230 pub last_update: Option<String>,
231 pub outcome: String,
232 #[serde(default)]
233 pub bucket_index: Option<u32>,
234 pub owner: Address,
235 pub maker_address: Option<String>,
236 #[serde(default)]
237 pub maker_orders: Vec<MakerOrder>,
238 pub transaction_hash: String,
239 pub trader_side: Option<String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct MakerOrder {
245 pub order_id: String,
246 pub owner: String,
247 pub maker_address: String,
248 pub matched_amount: String,
249 pub price: String,
250 pub fee_rate_bps: String,
251 pub asset_id: String,
252 pub outcome: String,
253 pub side: OrderSide,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct ListTradesResponse {
259 pub data: Vec<Trade>,
260 pub next_cursor: Option<String>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct BuilderTrade {
270 pub id: String,
271 pub trade_type: String,
272 pub taker_order_hash: String,
273 pub builder: String,
274 pub market: String,
275 pub asset_id: String,
276 pub side: String,
277 pub size: String,
278 pub size_usdc: String,
279 pub price: String,
280 pub status: String,
281 pub outcome: String,
282 pub outcome_index: u32,
283 pub owner: String,
284 pub maker: String,
285 pub transaction_hash: String,
286 pub match_time: String,
287 #[serde(default)]
288 pub bucket_index: Option<u32>,
289 pub fee: String,
290 pub fee_usdc: String,
291 #[serde(rename = "err_msg")]
292 pub err_msg: Option<String>,
293 pub created_at: Option<String>,
294 pub updated_at: Option<String>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct ListBuilderTradesResponse {
300 pub data: Vec<BuilderTrade>,
301 pub next_cursor: Option<String>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct BalanceAllowanceResponse {
307 pub balance: String,
308 pub allowances: HashMap<String, String>,
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn trade_deserialization() {
317 let json = r#"{
318 "id": "trade-123",
319 "taker_order_id": "order-456",
320 "market": "0xcondition",
321 "asset_id": "0xtoken",
322 "side": "BUY",
323 "size": "100.5",
324 "fee_rate_bps": "0",
325 "price": "0.55",
326 "status": "MATCHED",
327 "match_time": "1700000000",
328 "last_update": null,
329 "outcome": "Yes",
330 "bucket_index": null,
331 "owner": "0x0000000000000000000000000000000000000001",
332 "transaction_hash": "0xhash123"
333 }"#;
334 let trade: Trade = serde_json::from_str(json).unwrap();
335 assert_eq!(trade.id, "trade-123");
336 assert_eq!(trade.side, OrderSide::Buy);
337 assert_eq!(trade.price, "0.55");
338 assert!(trade.last_update.is_none());
339 assert!(trade.bucket_index.is_none());
340 assert!(trade.maker_address.is_none());
342 assert!(trade.maker_orders.is_empty());
343 assert!(trade.trader_side.is_none());
344 }
345
346 #[test]
347 fn trade_with_optional_fields() {
348 let json = r#"{
349 "id": "t1",
350 "taker_order_id": "o1",
351 "market": "0xcond",
352 "asset_id": "0xasset",
353 "side": "SELL",
354 "size": "50",
355 "fee_rate_bps": "100",
356 "price": "0.72",
357 "status": "MATCHED",
358 "match_time": "1700001000",
359 "last_update": "1700002000",
360 "outcome": "No",
361 "bucket_index": 3,
362 "owner": "0x0000000000000000000000000000000000000002",
363 "maker_address": "0xmaker",
364 "maker_orders": [{
365 "order_id": "mo-1",
366 "owner": "0xowner",
367 "maker_address": "0xmaker",
368 "matched_amount": "50",
369 "price": "0.72",
370 "fee_rate_bps": "100",
371 "asset_id": "0xasset",
372 "outcome": "No",
373 "side": "BUY"
374 }],
375 "transaction_hash": "0xhash456",
376 "trader_side": "TAKER"
377 }"#;
378 let trade: Trade = serde_json::from_str(json).unwrap();
379 assert_eq!(trade.side, OrderSide::Sell);
380 assert_eq!(trade.last_update.as_deref(), Some("1700002000"));
381 assert_eq!(trade.bucket_index, Some(3));
382 assert_eq!(trade.maker_address.as_deref(), Some("0xmaker"));
383 assert_eq!(trade.maker_orders.len(), 1);
384 assert_eq!(trade.maker_orders[0].order_id, "mo-1");
385 assert_eq!(trade.maker_orders[0].matched_amount, "50");
386 assert_eq!(trade.maker_orders[0].side, OrderSide::Buy);
387 assert_eq!(trade.trader_side.as_deref(), Some("TAKER"));
388 }
389
390 #[test]
391 fn list_trades_response_deserializes() {
392 let json = r#"{
393 "data": [{
394 "id": "t1",
395 "taker_order_id": "o1",
396 "market": "0xcond",
397 "asset_id": "0xasset",
398 "side": "BUY",
399 "size": "100",
400 "fee_rate_bps": "0",
401 "price": "0.55",
402 "status": "MATCHED",
403 "match_time": "1700000000",
404 "outcome": "Yes",
405 "owner": "0x0000000000000000000000000000000000000001",
406 "transaction_hash": "0xhash"
407 }],
408 "next_cursor": "abc123"
409 }"#;
410 let resp: ListTradesResponse = serde_json::from_str(json).unwrap();
411 assert_eq!(resp.data.len(), 1);
412 assert_eq!(resp.data[0].id, "t1");
413 assert_eq!(resp.next_cursor.as_deref(), Some("abc123"));
414 }
415
416 #[test]
417 fn list_trades_response_empty() {
418 let json = r#"{"data": [], "next_cursor": null}"#;
419 let resp: ListTradesResponse = serde_json::from_str(json).unwrap();
420 assert!(resp.data.is_empty());
421 assert!(resp.next_cursor.is_none());
422 }
423
424 #[test]
425 fn builder_trade_deserialization() {
426 let json = r#"{
427 "id": "bt-1",
428 "tradeType": "LIMIT",
429 "takerOrderHash": "0xhash",
430 "builder": "0xbuilder",
431 "market": "0xcond",
432 "assetId": "0xtoken",
433 "side": "BUY",
434 "size": "100",
435 "sizeUsdc": "55.00",
436 "price": "0.55",
437 "status": "MATCHED",
438 "outcome": "Yes",
439 "outcomeIndex": 0,
440 "owner": "0xowner",
441 "maker": "0xmaker",
442 "transactionHash": "0xtxhash",
443 "matchTime": "1700000000",
444 "bucketIndex": 5,
445 "fee": "0.01",
446 "feeUsdc": "0.55",
447 "err_msg": null,
448 "createdAt": "2024-01-01T00:00:00Z",
449 "updatedAt": "2024-01-01T00:00:01Z"
450 }"#;
451 let bt: BuilderTrade = serde_json::from_str(json).unwrap();
452 assert_eq!(bt.id, "bt-1");
453 assert_eq!(bt.trade_type, "LIMIT");
454 assert_eq!(bt.builder, "0xbuilder");
455 assert_eq!(bt.size_usdc, "55.00");
456 assert_eq!(bt.outcome_index, 0);
457 assert_eq!(bt.bucket_index, Some(5));
458 assert!(bt.err_msg.is_none());
459 assert_eq!(bt.created_at.as_deref(), Some("2024-01-01T00:00:00Z"));
460 }
461
462 #[test]
463 fn builder_trade_with_error() {
464 let json = r#"{
465 "id": "bt-2",
466 "tradeType": "MARKET",
467 "takerOrderHash": "0x",
468 "builder": "0x",
469 "market": "0x",
470 "assetId": "0x",
471 "side": "SELL",
472 "size": "0",
473 "sizeUsdc": "0",
474 "price": "0",
475 "status": "FAILED",
476 "outcome": "No",
477 "outcomeIndex": 1,
478 "owner": "0x",
479 "maker": "0x",
480 "transactionHash": "0x",
481 "matchTime": "0",
482 "fee": "0",
483 "feeUsdc": "0",
484 "err_msg": "insufficient balance",
485 "createdAt": null,
486 "updatedAt": null
487 }"#;
488 let bt: BuilderTrade = serde_json::from_str(json).unwrap();
489 assert_eq!(bt.err_msg.as_deref(), Some("insufficient balance"));
490 assert!(bt.bucket_index.is_none());
491 assert!(bt.created_at.is_none());
492 }
493
494 #[test]
495 fn list_builder_trades_response_deserializes() {
496 let json = r#"{
497 "data": [{
498 "id": "bt-1",
499 "tradeType": "LIMIT",
500 "takerOrderHash": "0x",
501 "builder": "0x",
502 "market": "0x",
503 "assetId": "0x",
504 "side": "BUY",
505 "size": "100",
506 "sizeUsdc": "55",
507 "price": "0.55",
508 "status": "MATCHED",
509 "outcome": "Yes",
510 "outcomeIndex": 0,
511 "owner": "0x",
512 "maker": "0x",
513 "transactionHash": "0x",
514 "matchTime": "0",
515 "fee": "0",
516 "feeUsdc": "0",
517 "createdAt": null,
518 "updatedAt": null
519 }],
520 "next_cursor": "cursor123"
521 }"#;
522 let resp: ListBuilderTradesResponse = serde_json::from_str(json).unwrap();
523 assert_eq!(resp.data.len(), 1);
524 assert_eq!(resp.data[0].id, "bt-1");
525 assert_eq!(resp.next_cursor.as_deref(), Some("cursor123"));
526 }
527
528 #[test]
529 fn balance_allowance_response_deserializes() {
530 let json = r#"{"balance": "141171137", "allowances": {"0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E": "999999"}}"#;
531 let resp: BalanceAllowanceResponse = serde_json::from_str(json).unwrap();
532 assert_eq!(resp.balance, "141171137");
533 assert_eq!(
534 resp.allowances
535 .get("0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E")
536 .unwrap(),
537 "999999"
538 );
539 }
540}