mirror of
https://github.com/floor-licker/polyfill-rs.git
synced 2026-08-22 00:48:08 +00:00
Merge pull request #54 from floor-licker/fix/typed-pagination-pages
fix: parse paginated data through typed pages
This commit is contained in:
+132
-34
@@ -21,7 +21,7 @@ use reqwest::{Method, RequestBuilder, Response};
|
|||||||
use rust_decimal::prelude::FromPrimitive;
|
use rust_decimal::prelude::FromPrimitive;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -46,6 +46,12 @@ struct PriceRequest<'a> {
|
|||||||
side: &'a str,
|
side: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DataPage<T> {
|
||||||
|
data: Vec<T>,
|
||||||
|
next_cursor: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn polymarket_default_headers() -> HeaderMap {
|
fn polymarket_default_headers() -> HeaderMap {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
@@ -1389,34 +1395,18 @@ impl ClobClient {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
||||||
|
|
||||||
let resp = r
|
let page = r
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
||||||
.json::<Value>()
|
.json::<DataPage<crate::types::OpenOrder>>()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
PolyfillError::parse(format!("Failed to parse response: {}", e), None)
|
PolyfillError::parse(format!("Failed to parse response: {}", e), None)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let new_cursor = resp["next_cursor"]
|
next_cursor = page.next_cursor;
|
||||||
.as_str()
|
output.extend(page.data);
|
||||||
.ok_or_else(|| {
|
|
||||||
PolyfillError::parse("Failed to parse next cursor".to_string(), None)
|
|
||||||
})?
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
next_cursor = new_cursor;
|
|
||||||
|
|
||||||
let results = resp["data"].clone();
|
|
||||||
let orders =
|
|
||||||
serde_json::from_value::<Vec<crate::types::OpenOrder>>(results).map_err(|e| {
|
|
||||||
PolyfillError::parse(
|
|
||||||
format!("Failed to parse data from order response: {}", e),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
output.extend(orders);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(output)
|
Ok(output)
|
||||||
@@ -1472,27 +1462,18 @@ impl ClobClient {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
|
||||||
|
|
||||||
let resp = r
|
let page = r
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
.map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
|
||||||
.json::<Value>()
|
.json::<DataPage<Value>>()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
PolyfillError::parse(format!("Failed to parse response: {}", e), None)
|
PolyfillError::parse(format!("Failed to parse response: {}", e), None)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let new_cursor = resp["next_cursor"]
|
next_cursor = page.next_cursor;
|
||||||
.as_str()
|
output.extend(page.data);
|
||||||
.ok_or_else(|| {
|
|
||||||
PolyfillError::parse("Failed to parse next cursor".to_string(), None)
|
|
||||||
})?
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
next_cursor = new_cursor;
|
|
||||||
|
|
||||||
let results = resp["data"].clone();
|
|
||||||
output.push(results);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(output)
|
Ok(output)
|
||||||
@@ -3502,6 +3483,123 @@ mod tests {
|
|||||||
assert_eq!(cancel_all.canceled, vec!["order-9".to_string()]);
|
assert_eq!(cancel_all.canceled, vec!["order-9".to_string()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_orders_parses_typed_pages() {
|
||||||
|
let mut server = Server::new_async().await;
|
||||||
|
let first_page = server
|
||||||
|
.mock("GET", "/data/orders")
|
||||||
|
.match_query(Matcher::UrlEncoded("next_cursor".into(), "MA==".into()))
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(
|
||||||
|
r#"{
|
||||||
|
"data": [{
|
||||||
|
"associate_trades": ["trade-1"],
|
||||||
|
"id": "order-1",
|
||||||
|
"status": "LIVE",
|
||||||
|
"market": "market-1",
|
||||||
|
"original_size": "10",
|
||||||
|
"outcome": "Yes",
|
||||||
|
"maker_address": "0x1111111111111111111111111111111111111111",
|
||||||
|
"owner": "0x2222222222222222222222222222222222222222",
|
||||||
|
"price": "0.55",
|
||||||
|
"side": "BUY",
|
||||||
|
"size_matched": "1.5",
|
||||||
|
"asset_id": "asset-1",
|
||||||
|
"expiration": "0",
|
||||||
|
"type": "GTC",
|
||||||
|
"created_at": "1713916800"
|
||||||
|
}],
|
||||||
|
"next_cursor": "CURSOR1"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
let second_page = server
|
||||||
|
.mock("GET", "/data/orders")
|
||||||
|
.match_query(Matcher::UrlEncoded("next_cursor".into(), "CURSOR1".into()))
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(
|
||||||
|
r#"{
|
||||||
|
"data": [{
|
||||||
|
"associate_trades": [],
|
||||||
|
"id": "order-2",
|
||||||
|
"status": "LIVE",
|
||||||
|
"market": "market-1",
|
||||||
|
"original_size": "20",
|
||||||
|
"outcome": "No",
|
||||||
|
"maker_address": "0x1111111111111111111111111111111111111111",
|
||||||
|
"owner": "0x2222222222222222222222222222222222222222",
|
||||||
|
"price": "0.45",
|
||||||
|
"side": "SELL",
|
||||||
|
"size_matched": "0",
|
||||||
|
"asset_id": "asset-2",
|
||||||
|
"expiration": "0",
|
||||||
|
"type": "GTC",
|
||||||
|
"created_at": "1713916810"
|
||||||
|
}],
|
||||||
|
"next_cursor": "LTE="
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let client = create_test_client_with_l2_auth(&server.url());
|
||||||
|
let orders = client.get_orders(None, None).await.unwrap();
|
||||||
|
|
||||||
|
first_page.assert_async().await;
|
||||||
|
second_page.assert_async().await;
|
||||||
|
assert_eq!(orders.len(), 2);
|
||||||
|
assert_eq!(orders[0].id, "order-1");
|
||||||
|
assert_eq!(orders[1].id, "order-2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_get_trades_flattens_paginated_data() {
|
||||||
|
let mut server = Server::new_async().await;
|
||||||
|
let first_page = server
|
||||||
|
.mock("GET", "/data/trades")
|
||||||
|
.match_query(Matcher::UrlEncoded("next_cursor".into(), "MA==".into()))
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(
|
||||||
|
r#"{
|
||||||
|
"data": [{"id": "trade-1", "price": "0.55"}],
|
||||||
|
"next_cursor": "CURSOR1"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
let second_page = server
|
||||||
|
.mock("GET", "/data/trades")
|
||||||
|
.match_query(Matcher::UrlEncoded("next_cursor".into(), "CURSOR1".into()))
|
||||||
|
.with_status(200)
|
||||||
|
.with_header("content-type", "application/json")
|
||||||
|
.with_body(
|
||||||
|
r#"{
|
||||||
|
"data": [
|
||||||
|
{"id": "trade-2", "price": "0.45"},
|
||||||
|
{"id": "trade-3", "price": "0.60"}
|
||||||
|
],
|
||||||
|
"next_cursor": "LTE="
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.create_async()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let client = create_test_client_with_l2_auth(&server.url());
|
||||||
|
let trades = client.get_trades(None, None).await.unwrap();
|
||||||
|
|
||||||
|
first_page.assert_async().await;
|
||||||
|
second_page.assert_async().await;
|
||||||
|
assert_eq!(trades.len(), 3);
|
||||||
|
assert_eq!(trades[0]["id"], "trade-1");
|
||||||
|
assert_eq!(trades[1]["id"], "trade-2");
|
||||||
|
assert_eq!(trades[2]["id"], "trade-3");
|
||||||
|
assert!(trades.iter().all(|trade| trade.is_object()));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_get_fee_rate_bps_success() {
|
async fn test_get_fee_rate_bps_success() {
|
||||||
let mut server = Server::new_async().await;
|
let mut server = Server::new_async().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user