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