Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmake/PolymarketTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ if(POLYMARKET_CLIENT_BUILD_TESTS)
target_link_libraries(test_prepared_order PRIVATE polymarket::client)
add_test(NAME test_prepared_order COMMAND test_prepared_order)

add_executable(test_pagination tests/test_pagination.cpp)
target_link_libraries(test_pagination PRIVATE polymarket::client)
add_test(NAME test_pagination COMMAND test_pagination)

add_executable(test_clob_pagers tests/test_clob_pagers.cpp)
target_link_libraries(test_clob_pagers PRIVATE polymarket::client)
add_test(NAME test_clob_pagers COMMAND test_clob_pagers)

add_executable(test_sdk_error tests/test_sdk_error.cpp)
target_link_libraries(test_sdk_error PRIVATE polymarket::client)
add_test(NAME test_sdk_error COMMAND test_sdk_error)
Expand Down
25 changes: 23 additions & 2 deletions include/polymarket/clob_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "polymarket/types.hpp"
#include "polymarket/http_client.hpp"
#include "polymarket/order_signer.hpp"
#include "polymarket/pagination.hpp"
#include "polymarket/sdk_error.hpp"
#include <string>
#include <vector>
Expand Down Expand Up @@ -80,6 +81,14 @@ namespace polymarket
ClobMarketPage get_simplified_markets(const std::string &next_cursor = "");
ClobMarketPage get_sampling_simplified_markets(const std::string &next_cursor = "");

// Pagers fetch lazily, starting at `start_cursor` (or the first page),
// and can resume from resume_token(). This client must outlive them.
Paginator<ClobMarket> markets_pager(const std::string &start_cursor = "");
Paginator<ClobMarket> sampling_markets_pager(const std::string &start_cursor = "");
Paginator<ClobMarket> simplified_markets_pager(const std::string &start_cursor = "");
Paginator<ClobMarket>
sampling_simplified_markets_pager(const std::string &start_cursor = "");

// Orderbook
std::optional<Orderbook> get_order_book(const std::string &token_id);
std::map<std::string, Orderbook> get_order_books(const std::vector<std::string> &token_ids);
Expand Down Expand Up @@ -186,6 +195,10 @@ namespace polymarket
std::vector<OpenOrder> get_open_orders(const std::string &market = "");
Result<std::vector<OpenOrder>> get_open_orders_result(const std::string &market = "");
std::vector<Trade> get_trades(const std::string &next_cursor = "");
// Lazy pagers; fail with Auth on the first page when not authenticated.
Paginator<OpenOrder> open_orders_pager(const std::string &market = "",
const std::string &start_cursor = "");
Paginator<Trade> trades_pager(const std::string &start_cursor = "");
// Empty when the account has no trade with this ID (yet).
std::optional<Trade> get_trade(const std::string &trade_id);
Result<std::optional<Trade>> get_trade_result(const std::string &trade_id);
Expand Down Expand Up @@ -385,8 +398,16 @@ namespace polymarket

// Get positions that can be merged (user holds both Yes and No outcomes)
std::vector<Position> get_mergeable_positions(const std::string &user_address = "");

private:
// Offset pagers over the same queries; truncated() is set at the Data
// API offset cap. Fail with InvalidArgument when no address resolves.
Paginator<Position> positions_pager(const std::string &user_address = "",
size_t start_offset = 0);
Paginator<Position> redeemable_positions_pager(const std::string &user_address = "",
size_t start_offset = 0);
Paginator<Position> mergeable_positions_pager(const std::string &user_address = "",
size_t start_offset = 0);

private:
HttpClient http_;
HttpClient order_http_;
HttpClient data_http_;
Expand Down
309 changes: 309 additions & 0 deletions include/polymarket/pagination.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
#pragma once

#include "polymarket/sdk_error.hpp"
#include <algorithm>
#include <cstddef>
#include <functional>
#include <iterator>
#include <limits>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>

namespace polymarket
{
namespace detail
{
class OpaqueCursorPaginationError : public std::runtime_error
{
public:
using std::runtime_error::runtime_error;
};

class OpaqueCursorPagination
{
public:
static constexpr size_t max_pages = 1000;

OpaqueCursorPagination(std::string initial_cursor, std::string end_cursor)
: cursor_(std::move(initial_cursor)), end_cursor_(std::move(end_cursor))
{
}

const std::string &cursor() const { return cursor_; }

bool begin_page()
{
if (cursor_.empty() || cursor_ == end_cursor_) return false;
if (pages_started_ >= max_pages)
throw OpaqueCursorPaginationError(
"opaque cursor pagination exceeded 1000 pages");
if (!seen_cursors_.insert(cursor_).second)
throw OpaqueCursorPaginationError("opaque cursor pagination cycle detected");
++pages_started_;
return true;
}

void advance(std::string next_cursor)
{
if (next_cursor.empty() || next_cursor == end_cursor_)
{
cursor_ = end_cursor_;
return;
}
if (seen_cursors_.contains(next_cursor))
throw OpaqueCursorPaginationError("opaque cursor pagination cycle detected");
cursor_ = std::move(next_cursor);
}

private:
std::string cursor_;
std::string end_cursor_;
size_t pages_started_{0};
std::unordered_set<std::string> seen_cursors_;
};
} // namespace detail

// The next page to request. Cursor pagination fills `cursor`; offset
// pagination fills `offset` and `limit`. Pass it back to resume later; an
// exhausted cursor holds the end cursor, and an offset past the cap
// resumes as truncated without fetching.
struct PageRequest
{
std::string cursor;
size_t offset{0};
size_t limit{0};
};

// One fetched page. `next_cursor` is read only by cursor pagination.
template <typename T> struct Page
{
std::vector<T> items;
std::string next_cursor;
};

// Pulls pages lazily through `fetch`. Cursor pagination ends at an empty or
// end cursor and fails after max_pages or on a cycle; the page whose next
// cursor repeats is still returned and the call after it fails. Offset pagination ends
// on a short page and stops with truncated() set once the next offset would
// pass `max_offset`; items inserted between requests can shift across pages.
// After a failure every call reports the same error. Not thread-safe; any
// client captured by `fetch` must outlive the paginator.
template <typename T> class Paginator
{
public:
using Fetch = std::function<Result<Page<T>>(const PageRequest &)>;
class ItemRange;

static Paginator cursor(std::string endpoint, Fetch fetch, std::string initial_cursor,
std::string end_cursor)
{
Paginator paginator(std::move(endpoint), std::move(fetch));
paginator.cursor_.emplace(std::move(initial_cursor), std::move(end_cursor));
return paginator;
}

static Paginator offset(std::string endpoint, Fetch fetch, size_t limit, size_t max_offset,
size_t start_offset = 0)
{
if (limit == 0) throw std::invalid_argument("pagination limit must be positive");
Paginator paginator(std::move(endpoint), std::move(fetch));
paginator.limit_ = limit;
paginator.max_offset_ = max_offset;
paginator.offset_ = start_offset;
paginator.truncated_ = start_offset > max_offset;
paginator.done_ = paginator.truncated_;
return paginator;
}

// nullopt once exhausted. A page may be empty.
Result<std::optional<std::vector<T>>> next_page()
{
using PageResult = Result<std::optional<std::vector<T>>>;
if (error_) return PageResult::failure(*error_);
if (done_) return PageResult::success(std::nullopt);
return cursor_ ? next_cursor_page() : next_offset_page();
}

// Drains remaining pages, keeping at most `max_items`. resume_token()
// then points past the last fetched page, not past the last kept item.
Result<std::vector<T>> collect(size_t max_items = std::numeric_limits<size_t>::max())
{
std::vector<T> items;
while (items.size() < max_items)
{
auto page = next_page();
if (!page.ok()) return Result<std::vector<T>>::failure(page.error());
if (!page.value()) break;
auto &batch = *page.value();
const size_t keep = std::min(batch.size(), max_items - items.size());
items.insert(
items.end(), std::make_move_iterator(batch.begin()),
std::make_move_iterator(batch.begin() + static_cast<std::ptrdiff_t>(keep)));
}
return Result<std::vector<T>>::success(std::move(items));
}

// Range-for over remaining items. Iteration stops on failure; check
// error() afterwards. After a break, items() resumes at the item last
// returned. next_page() and collect() skip items still buffered here.
// The paginator must stay in place while iterating.
ItemRange items() { return ItemRange(*this); }

const std::optional<SdkError> &error() const { return error_; }
bool truncated() const { return truncated_; }
bool done() const { return done_ || error_.has_value(); }

PageRequest resume_token() const
{
if (cursor_) return PageRequest{cursor_->cursor(), 0, 0};
return PageRequest{"", offset_, limit_};
}

class ItemIterator
{
public:
using value_type = T;
using difference_type = std::ptrdiff_t;

ItemIterator() = default;
explicit ItemIterator(Paginator &paginator) : paginator_(&paginator)
{
if (paginator_->buffer_index_ >= paginator_->buffer_.size())
paginator_->fill_buffer();
}

T &operator*() const { return paginator_->buffer_[paginator_->buffer_index_]; }
T *operator->() const { return &**this; }

ItemIterator &operator++()
{
if (++paginator_->buffer_index_ >= paginator_->buffer_.size())
paginator_->fill_buffer();
return *this;
}
void operator++(int) { ++*this; }

friend bool operator==(const ItemIterator &it, std::default_sentinel_t)
{
return it.at_end();
}

private:
bool at_end() const
{
return paginator_ == nullptr ||
paginator_->buffer_index_ >= paginator_->buffer_.size();
}

Paginator *paginator_{nullptr};
};

class ItemRange
{
public:
explicit ItemRange(Paginator &paginator) : paginator_(&paginator) {}
ItemIterator begin() const { return ItemIterator(*paginator_); }
std::default_sentinel_t end() const { return {}; }

private:
Paginator *paginator_;
};

private:
Paginator(std::string endpoint, Fetch fetch)
: endpoint_(std::move(endpoint)), fetch_(std::move(fetch))
{
if (!fetch_) throw std::invalid_argument("pagination fetch must be set");
}

Result<std::optional<std::vector<T>>> fail(SdkError error)
{
error_ = std::move(error);
return Result<std::optional<std::vector<T>>>::failure(*error_);
}

Result<std::optional<std::vector<T>>> next_cursor_page()
{
using PageResult = Result<std::optional<std::vector<T>>>;
try
{
if (!cursor_->begin_page())
{
done_ = true;
return PageResult::success(std::nullopt);
}
}
catch (const detail::OpaqueCursorPaginationError &ex)
{
return fail(make_parse_error(ex.what(), endpoint_));
}

auto page = fetch_(PageRequest{cursor_->cursor(), 0, 0});
if (!page.ok()) return fail(page.error());
try
{
cursor_->advance(std::move(page.value().next_cursor));
}
catch (const detail::OpaqueCursorPaginationError &ex)
{
error_ = make_parse_error(ex.what(), endpoint_);
}
return PageResult::success(std::move(page.value().items));
}

Result<std::optional<std::vector<T>>> next_offset_page()
{
using PageResult = Result<std::optional<std::vector<T>>>;
auto page = fetch_(PageRequest{"", offset_, limit_});
if (!page.ok()) return fail(page.error());
auto &items = page.value().items;
if (items.size() > limit_)
return fail(make_parse_error("page holds " + std::to_string(items.size()) +
" items, more than limit " +
std::to_string(limit_),
endpoint_));
const bool short_page = items.size() < limit_;
const bool at_offset_cap = !short_page && max_offset_ - offset_ < limit_;
offset_ = items.size() > std::numeric_limits<size_t>::max() - offset_
? std::numeric_limits<size_t>::max()
: offset_ + items.size();
if (short_page)
done_ = true;
else if (at_offset_cap)
done_ = truncated_ = true;
return PageResult::success(std::move(items));
}

void fill_buffer()
{
buffer_.clear();
buffer_index_ = 0;
while (true)
{
auto page = next_page();
if (!page.ok() || !page.value()) return;
if (!page.value()->empty())
{
buffer_ = std::move(*page.value());
return;
}
}
}

std::string endpoint_;
Fetch fetch_;
std::optional<detail::OpaqueCursorPagination> cursor_;
size_t offset_{0};
size_t limit_{0};
size_t max_offset_{0};
bool done_{false};
bool truncated_{false};
std::optional<SdkError> error_;
std::vector<T> buffer_;
size_t buffer_index_{0};
};
} // namespace polymarket
Loading
Loading