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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ set(POLYMARKET_CLIENT_SOURCES
src/polymarket_events.cpp
src/polymarket_contracts.cpp
src/position_calls.cpp
src/approval_calls.cpp
src/trading_approvals.cpp
src/position_gamma.cpp
src/position_client.cpp
src/position_senders.cpp
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ Reusable C++20 client for Polymarket: REST, WebSocket streaming, and order signi
- **Decimal math**: shared scaled-integer conversion for trading amounts.
- **Structured errors**: opt-in `Result<T>` APIs with typed SDK error classification.
- **EVM JSON-RPC**: Polygon HTTP catch-up and WebSocket subscriptions for logs, heads, and pending transaction hashes, plus transaction calls (nonce, gas, `eth_call`, raw send, receipts).
- **On-chain positions**: `PositionClient` splits, merges and redeems CTF and Protocol V2 positions from an EOA or, gaslessly through the Polymarket relayer, from a Gnosis Safe. Built on an ABI encoder, EIP-155 transaction signing, and a `PolymarketContracts` address registry.
- **On-chain positions**: `PositionClient` splits, merges and redeems CTF and Protocol V2 positions, and checks and sets the one-time trading approvals, from an EOA or, gaslessly through the Polymarket relayer, from a Gnosis Safe. Built on an ABI encoder, EIP-155 transaction signing, and a `PolymarketContracts` address registry.
- **Resolution Events**: Decoders for UMA adapter and Conditional Tokens resolution/redemption logs.
- **Proxy Support**: HTTP/HTTPS proxy with authentication for geo-restricted access.
- **Neg-Risk Markets**: Automatic exchange selection for neg_risk markets.
- **Examples**: REST (`rest_example`), signing (`sign_example`), WebSocket (`ws_example`), on-chain positions (`position_example`), onchain watchers.
- **Examples**: REST (`rest_example`), signing (`sign_example`), WebSocket (`ws_example`), on-chain positions (`position_example`), trading approvals (`approvals_example`), onchain watchers.
- **Benchmarks and tests**: local benchmark targets plus `ctest` coverage.

## Requirements
Expand Down Expand Up @@ -155,6 +155,7 @@ int main() {
- `ws_example`: connect to Polymarket WS and subscribe to orderbook agg
- `user_stream_example`: stream your own order and trade events (requires `PRIVATE_KEY`; optional condition IDs as arguments)
- `position_example`: split, merge or redeem a position from an EOA or Safe; dry run unless `--execute` (requires `PRIVATE_KEY`, `POLYGON_RPC_ENDPOINT`)
- `approvals_example`: list a wallet's missing trading approvals and grant them; dry run unless `--execute` (requires `PRIVATE_KEY`, `POLYGON_RPC_ENDPOINT`)
- `uma_oracle_watch`: stream UMA adapter lifecycle events over Polygon JSON-RPC
- `condition_resolution_watch`: stream Conditional Tokens resolution/redemption events
- `evm_event_indexer_example`: persistent HTTP catch-up + live WS indexer with a cursor file
Expand Down Expand Up @@ -203,8 +204,10 @@ polymarket::PositionClient client(config);
client.merge_positions(condition_id, "max").wait();
```

A new wallet needs the trading approvals once; `client.setup_trading_approvals()`
grants whichever of them `client.get_trading_approvals_state()` reports missing.
See [docs/position-operations.md](docs/position-operations.md) for wallet
types, batching and atomicity, errors, and required approvals.
types, batching and atomicity, errors, and the approval set.

## Structured Errors

Expand Down Expand Up @@ -285,7 +288,7 @@ required.

## Tests

`test_utils` exercises basic utility helpers. `test_evm_events` covers EVM topic hashing, log filter serialization, and UMA/CTF event decoding. `test_evm_event_indexer` covers block range planning and file-backed cursors. Transport, order execution, typed error, signing, and WebSocket resilience tests are included when `POLYMARKET_CLIENT_BUILD_TESTS=ON`. `test_evm_abi`, `test_evm_abi_shapes`, `test_evm_transaction`, `test_json_rpc_transactions`, `test_polymarket_contracts`, `test_position_calls`, `test_position_client`, `test_safe_relayer`, `test_safe_relayer_flows` and `test_transaction_waiters` check ABI encoding, transaction signing, Safe signing and relayer payloads against vectors from the official Python SDK, and run position flows against local fake RPC, Gamma and relayer servers. Run via `ctest --test-dir build`.
`test_utils` exercises basic utility helpers. `test_evm_events` covers EVM topic hashing, log filter serialization, and UMA/CTF event decoding. `test_evm_event_indexer` covers block range planning and file-backed cursors. Transport, order execution, typed error, signing, and WebSocket resilience tests are included when `POLYMARKET_CLIENT_BUILD_TESTS=ON`. `test_evm_abi`, `test_evm_abi_shapes`, `test_evm_transaction`, `test_json_rpc_transactions`, `test_polymarket_contracts`, `test_position_calls`, `test_position_client`, `test_safe_relayer`, `test_safe_relayer_flows`, `test_approval_calls`, `test_trading_approvals` and `test_transaction_waiters` check ABI encoding, transaction signing, Safe signing and relayer payloads against vectors from the official Python SDK, and run position flows against local fake RPC, Gamma and relayer servers. Run via `ctest --test-dir build`.

`test_oracle_watcher` validates in-memory normalization against synthetic UMA/CTF fixtures.
`test_oracle_watcher_historical` is labeled `live` and only performs its
Expand All @@ -310,6 +313,7 @@ Set these env vars explicitly if you need another RPC or contract set.
- `src/polymarket_contracts.cpp`: Polygon mainnet contract registry
- `src/position_client.cpp`, `src/position_calls.cpp`, `src/position_gamma.cpp`: split/merge/redeem, calldata and Gamma market resolution
- `src/position_senders.cpp`, `src/transaction_handle.cpp`: EOA/Safe sending and waiting for outcomes
- `src/trading_approvals.cpp`, `src/approval_calls.cpp`: trading approval state and setup, ERC-20/ERC-1155 approvals and transfers
- `src/safe_relayer.cpp`: Gnosis Safe signing and relayer submission
- `src/evm_event_indexer.cpp`: persistent log catch-up and live indexing
- `src/evm_utils.cpp`: ABI/log utilities
Expand Down
3 changes: 3 additions & 0 deletions cmake/PolymarketExamples.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ if(POLYMARKET_CLIENT_BUILD_EXAMPLES)
add_executable(position_example examples/position_example.cpp)
target_link_libraries(position_example PRIVATE polymarket::client)

add_executable(approvals_example examples/approvals_example.cpp)
target_link_libraries(approvals_example PRIVATE polymarket::client)

add_executable(uma_oracle_watch examples/uma_oracle_watch.cpp)
target_link_libraries(uma_oracle_watch PRIVATE polymarket::client)

Expand Down
15 changes: 15 additions & 0 deletions cmake/PolymarketTests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ if(POLYMARKET_CLIENT_BUILD_TESTS)
target_link_libraries(test_position_calls PRIVATE polymarket::client)
add_test(NAME test_position_calls COMMAND test_position_calls)

add_executable(test_approval_calls tests/test_approval_calls.cpp)
target_include_directories(test_approval_calls PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_approval_calls PRIVATE polymarket::client)
add_test(NAME test_approval_calls COMMAND test_approval_calls)

add_executable(test_trading_approvals tests/test_trading_approvals.cpp)
target_include_directories(test_trading_approvals PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_trading_approvals PRIVATE polymarket::client)
add_test(NAME test_trading_approvals COMMAND test_trading_approvals)

add_executable(test_trading_approvals_failures tests/test_trading_approvals_failures.cpp)
target_include_directories(test_trading_approvals_failures PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_trading_approvals_failures PRIVATE polymarket::client)
add_test(NAME test_trading_approvals_failures COMMAND test_trading_approvals_failures)

add_executable(test_transaction_waiters tests/test_transaction_waiters.cpp)
target_include_directories(test_transaction_waiters PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(test_transaction_waiters PRIVATE polymarket::client)
Expand Down
66 changes: 57 additions & 9 deletions docs/position-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,65 @@ calls throws `PartialBatchError` (see [Batches and atomicity](#batches-and-atomi

## Approvals

Operations do not check or set approvals. Before the first operation a wallet
needs:
Operations do not check approvals. Before the first operation a wallet needs:

- split: a pUSD `approve` for the operator contract (collateral adapter,
neg-risk collateral adapter or V2 router);
- merge and redeem: `setApprovalForAll` on the position token
(ConditionalTokens or the V2 position manager) for the same operator.

Wallets that have traded through polymarket.com may already have some of
these; check `allowance` and `isApprovedForAll` if unsure. For an EOA, a
missing approval shows up as a revert during gas estimation, before anything
is sent. For a Safe it surfaces only after submission, as a failed relayer
transaction.
For an EOA, a missing approval shows up as a revert during gas estimation,
before anything is sent. For a Safe it surfaces only after submission, as a
failed relayer transaction.

### Trading approvals

`setup_trading_approvals()` grants, once per wallet, the same set as the
official SDKs' `setup_trading_approvals` (`required_trading_approvals(contracts)`
in `include/polymarket/trading_approvals.hpp`): 17 approvals that cover CLOB
trading as well as split, merge and redeem.

| Token | Approval | Spenders / operators |
| ----------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| pUSD `collateral_token` | `approve(spender, 2^256 - 1)` | standard and neg-risk exchanges, both collateral adapters, V2 router, exchange V3, perps deposit contract |
| ConditionalTokens | `setApprovalForAll(operator, true)` | standard and neg-risk exchanges, both collateral adapters, auto-redeem operator, binary module, neg-risk module |
| V2 `position_manager` | `setApprovalForAll(operator, true)` | V2 router, exchange V3, auto-redeem operator |

```cpp
auto state = client.get_trading_approvals_state(); // or (other_wallet)
if (!state.is_fully_approved)
client.setup_trading_approvals(); // sends only state.missing, then waits
```

- `get_trading_approvals_state(wallet = this wallet)` reads every approval
with `eth_call` (`allowance`, `isApprovedForAll`) and lists only the missing
ones. An allowance below `2^256 - 1` counts as missing, as in the official
SDKs. The official SDKs read this from the Data API (`/v2/approvals`), which
can lag; this client always reads the chain.
- `setup_trading_approvals(timeout = 3 min)` sends `approve` calls first, then
`setApprovalForAll`, and waits. A Safe sends them as one atomic MultiSend
transaction; an EOA sends one transaction per approval (not atomic, see
[Batches and atomicity](#batches-and-atomicity); rerunning picks up where it
stopped). It returns `std::nullopt` without sending anything when the wallet
is already fully approved.

Single approvals and transfers return a `TransactionHandle` without waiting:

| Method | Call |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `approve_erc20(token, spender, amount)` | ERC-20 `approve`; `amount` is a uint256 in base units or `"max"`, `"0"` revokes |
| `approve_erc1155_for_all(token, operator, approved = true)` | ERC-1155 `setApprovalForAll` |
| `transfer_erc20(token, recipient, amount)` | ERC-20 `transfer` of a positive base-unit amount |

Each also takes an optional relayer `metadata` string.

## Example

```bash
# Trading approvals: list the missing ones, then grant them
./build/approvals_example
./build/approvals_example --execute

# Dry run: resolve the market and print balances
./build/position_example redeem 0x6b04...6fbc

Expand All @@ -145,7 +187,7 @@ transaction.
./build/position_example split 0x01...44 1000000 --execute
```

The example reads `PRIVATE_KEY` and `POLYGON_RPC_ENDPOINT`. With
The examples read `PRIVATE_KEY` and `POLYGON_RPC_ENDPOINT`. With
`POLYMARKET_PROXY_ADDRESS` set it uses the Safe and the relayer
(`RELAYER_API_KEY`, optional `RELAYER_API_KEY_ADDRESS`); otherwise it sends
from the EOA.
Expand All @@ -154,7 +196,7 @@ from the EOA.

- Lookup by market id or position id, and combo (multi-leg) positions.
- `POLY_PROXY` and deposit-wallet (`POLY_1271`) relayer paths.
- Setting trading approvals (`setup_trading_approvals`).
- Reading approvals from the Data API (`/v2/approvals`); state is read on chain.

## Verification

Expand All @@ -163,6 +205,9 @@ from the EOA.
by the official Python SDK (`test_evm_abi`, `test_evm_abi_shapes`,
`test_evm_transaction`, `test_position_calls`, `test_safe_relayer`).
- `test_position_client` and `test_safe_relayer_flows` run EOA and Safe flows;
`test_trading_approvals` runs approval reads, setup (EOA and Safe MultiSend),
single approvals and transfers; `test_approval_calls` checks approval and
transfer calldata against the py-sdk golden vectors;
`test_transaction_waiters` covers receipt and relayer waits whose timeout is
shorter than the poll interval.
- These flows run against local fake RPC, Gamma and relayer servers.
Expand All @@ -174,5 +219,8 @@ from the EOA.
`wait(1s, 2s)` on the pending split timed out after about 1.3 s.
- An EOA batch whose second call fails gas estimation threw
`PartialBatchError` with the mined hash of the first call.
- `setup_trading_approvals` on a Safe that lacked only the perps deposit
allowance sent that one `approve` through the relayer; a fresh
`get_trading_approvals_state` then reported the Safe fully approved.
- No Protocol V2 market was listed on Gamma yet, so 31-byte ids were only
checked against fixtures from the official Python SDK.
108 changes: 108 additions & 0 deletions examples/approvals_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#include "polymarket/position_client.hpp"

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>

// Check and set the one-time trading approvals of a wallet.
//
// approvals_example [--execute]
//
// Without --execute the example only reads which approvals are missing;
// nothing is sent. With --execute it grants the missing ones and waits for
// them to be mined.
//
// Environment: PRIVATE_KEY and POLYGON_RPC_ENDPOINT are required. With
// POLYMARKET_PROXY_ADDRESS set, the approvals are set on that Gnosis Safe
// through the relayer in one transaction (gasless; needs RELAYER_API_KEY,
// optional RELAYER_API_KEY_ADDRESS). Otherwise the EOA sends one transaction
// per missing approval and pays gas in POL.

namespace
{
std::string env(const char *name)
{
const char *value = std::getenv(name);
return value ? value : "";
}
} // namespace

int main(int argc, char **argv)
{
using namespace polymarket;

bool execute = false;
for (int i = 1; i < argc; ++i)
{
if (std::strcmp(argv[i], "--execute") != 0)
{
std::cerr << "usage: approvals_example [--execute]\n";
return 2;
}
execute = true;
}

PositionClientConfig config;
config.private_key = env("PRIVATE_KEY");
config.rpc_url = env("POLYGON_RPC_ENDPOINT");
if (config.private_key.empty() || config.rpc_url.empty())
{
std::cout << "PRIVATE_KEY or POLYGON_RPC_ENDPOINT not set; skipping approvals example.\n";
return 0;
}
config.funder_address = env("POLYMARKET_PROXY_ADDRESS");
if (!config.funder_address.empty())
{
config.wallet_type = SignatureType::POLY_GNOSIS_SAFE;
config.relayer_api_key = env("RELAYER_API_KEY");
config.relayer_api_key_address = env("RELAYER_API_KEY_ADDRESS");
}

try
{
PositionClient client(config);
const auto state = client.get_trading_approvals_state();
std::cout << "wallet " << client.wallet_address()
<< (config.wallet_type == SignatureType::EOA ? " (EOA)" : " (Safe via relayer)")
<< '\n';
for (const auto &approval : state.missing.erc20)
std::cout << "missing ERC-20 " << approval.token_address << " -> " << approval.spender
<< '\n';
for (const auto &approval : state.missing.erc1155)
std::cout << "missing ERC-1155 " << approval.token_address << " -> "
<< approval.operator_address << '\n';
if (state.is_fully_approved)
{
std::cout << "all trading approvals are set\n";
return 0;
}
if (!execute)
{
std::cout << "dry run: pass --execute to grant "
<< state.missing.erc20.size() + state.missing.erc1155.size()
<< " approval(s)\n";
return 0;
}

std::cout << "granting approvals and waiting for confirmation...\n";
const auto outcome = client.setup_trading_approvals();
if (outcome)
std::cout << "confirmed " << outcome->transaction_hash << " in block "
<< outcome->receipt.block_number << '\n';
}
catch (const PartialBatchError &error)
{
// EOA only: approvals already mined stay in place; rerun to finish.
std::cerr << "error: " << error.what() << '\n';
for (const auto &hash : error.submitted_hashes())
std::cerr << "already sent: " << hash << '\n';
return 1;
}
catch (const std::exception &error)
{
std::cerr << "error: " << error.what() << '\n';
return 1;
}
return 0;
}
3 changes: 3 additions & 0 deletions include/polymarket/polymarket_contracts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ namespace polymarket
std::string combinatorial_module;
std::string auto_redeem_operator;

// Perps collateral deposits; a pUSD spender in the trading approval set
std::string perps_deposit_contract;

// Wallet infrastructure
std::string proxy_factory;
std::string proxy_implementation;
Expand Down
Loading
Loading