Skip to content
Draft
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
22 changes: 22 additions & 0 deletions src/polymarket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,19 @@
Erc1155TradingApproval,
Event,
EventId,
FundingAddresses,
FundingAddressSet,
FundingAsset,
FundingAssetCatalog,
FundingFeeBreakdown,
FundingQuote,
FundingToken,
FundingTransaction,
FundingTransactionStatus,
FundingWarning,
GaslessTransaction,
Holder,
KnownFundingTransactionStatus,
LastTradePrice,
LastTradePriceForToken,
LeaderboardCategory,
Expand Down Expand Up @@ -306,6 +317,17 @@
"Event",
"EventId",
"EvmAddress",
"FundingAddressSet",
"FundingAddresses",
"FundingAsset",
"FundingAssetCatalog",
"FundingFeeBreakdown",
"FundingQuote",
"FundingToken",
"FundingTransaction",
"FundingTransactionStatus",
"FundingWarning",
"KnownFundingTransactionStatus",
"GaslessTransaction",
"GaslessTransactionHandle",
"HexString",
Expand Down
180 changes: 180 additions & 0 deletions src/polymarket/_internal/actions/funding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""Shared request construction and response parsing for account funding."""

from urllib.parse import quote

from eth_utils.address import to_checksum_address
from pydantic import Field, field_validator

from polymarket._internal.request import QueryParamValue
from polymarket._internal.validation import require_nonempty, validate_builder_code
from polymarket.errors import UserInputError
from polymarket.models.base import BaseModel
from polymarket.models.funding import (
FundingAddressSet,
FundingAssetCatalog,
FundingQuote,
FundingTransaction,
)
from polymarket.pagination import Page

_BUILDER_CODE_HEADER = "X-Builder-Code"
_DEFAULT_STATUS_PAGE_SIZE = 50
_MAX_STATUS_PAGE_SIZE = 100


class _FundingTransactionsPageResponse(BaseModel):
transactions: tuple[FundingTransaction, ...]
# Production may briefly return the pre-pagination shape while the new
# required nextCursor field rolls out. Treat omission as a terminal page.
next_cursor: str | None = Field(default=None, validation_alias="nextCursor")

@field_validator("next_cursor")
@classmethod
def _validate_next_cursor(cls, value: str | None) -> str | None:
if value == "":
raise ValueError("nextCursor must be non-empty or null")
return value


def _validate_evm_address(name: str, value: object) -> str:
validated = require_nonempty(name, value)
if len(validated) != 42 or not validated.startswith("0x"):
raise UserInputError(f"{name} must be a valid EVM address.")
try:
int(validated[2:], 16)
return to_checksum_address(validated)
except ValueError as error:
raise UserInputError(f"{name} must be a valid EVM address.") from error


def _require_nonblank(name: str, value: object) -> str:
validated = require_nonempty(name, value).strip()
if not validated:
raise UserInputError(f"{name} is required")
return validated


def _validate_positive_integer(name: str, value: object) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise UserInputError(f"{name} must be a positive integer.")
return value


def _builder_headers(builder_code: str | None) -> dict[str, str]:
if builder_code is None:
return {}
return {_BUILDER_CODE_HEADER: validate_builder_code(builder_code)}


def build_create_deposit_addresses_request(
*, wallet: str, builder_code: str | None = None
) -> tuple[str, dict[str, str], dict[str, str]]:
"""Build a request for chain-specific deposit addresses."""
return (
"/deposit",
{"address": _validate_evm_address("wallet", wallet)},
_builder_headers(builder_code),
)


def build_create_withdrawal_addresses_request(
*,
wallet: str,
destination_chain_id: int,
destination_token_address: str,
recipient_address: str,
builder_code: str | None = None,
) -> tuple[str, dict[str, str], dict[str, str]]:
"""Build a request for chain-specific withdrawal addresses."""
return (
"/withdraw",
{
"address": _validate_evm_address("wallet", wallet),
"toChainId": str(
_validate_positive_integer("destination_chain_id", destination_chain_id)
),
"toTokenAddress": _require_nonblank(
"destination_token_address", destination_token_address
),
"recipientAddr": _require_nonblank("recipient_address", recipient_address),
},
_builder_headers(builder_code),
)


def build_funding_quote_request(
*,
amount: int,
source_chain_id: int,
source_token_address: str,
destination_chain_id: int,
destination_token_address: str,
recipient_address: str,
) -> tuple[str, dict[str, str]]:
"""Build a request for a funding transfer quote."""
return (
"/quote",
{
"fromAmountBaseUnit": str(_validate_positive_integer("amount", amount)),
"fromChainId": str(_validate_positive_integer("source_chain_id", source_chain_id)),
"fromTokenAddress": _require_nonblank("source_token_address", source_token_address),
"recipientAddress": _require_nonblank("recipient_address", recipient_address),
"toChainId": str(
_validate_positive_integer("destination_chain_id", destination_chain_id)
),
"toTokenAddress": _require_nonblank(
"destination_token_address", destination_token_address
),
},
)


def build_list_funding_transactions_request(
*,
address: str,
page_size: int = _DEFAULT_STATUS_PAGE_SIZE,
cursor: str | None = None,
) -> tuple[str, dict[str, QueryParamValue]]:
"""Build a request for one page of funding transactions."""
validated = _require_nonblank("address", address)
if type(page_size) is not int:
raise UserInputError("page_size must be an int.")
if page_size < 1 or page_size > _MAX_STATUS_PAGE_SIZE:
raise UserInputError(f"page_size must be between 1 and {_MAX_STATUS_PAGE_SIZE}.")
params: dict[str, QueryParamValue] = {"limit": page_size}
if cursor is not None:
params["cursor"] = require_nonempty("cursor", cursor)
return f"/status/{quote(validated, safe='')}", params


def parse_funding_address_set(data: object) -> FundingAddressSet:
return FundingAddressSet.parse_response(data)


def parse_funding_asset_catalog(data: object) -> FundingAssetCatalog:
return FundingAssetCatalog.parse_response(data)


def parse_funding_quote(data: object) -> FundingQuote:
return FundingQuote.parse_response(data)


def parse_funding_transactions_page(data: object) -> Page[FundingTransaction]:
response = _FundingTransactionsPageResponse.parse_response(data)
return Page(
items=response.transactions,
has_more=response.next_cursor is not None,
next_cursor=response.next_cursor,
)


__all__ = [
"build_create_deposit_addresses_request",
"build_create_withdrawal_addresses_request",
"build_funding_quote_request",
"build_list_funding_transactions_request",
"parse_funding_address_set",
"parse_funding_asset_catalog",
"parse_funding_quote",
"parse_funding_transactions_page",
]
2 changes: 2 additions & 0 deletions src/polymarket/_internal/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class SyncClientContext:
environment_config: EnvironmentConfig = field(init=False, repr=False)
gamma: SyncTransport
data: SyncTransport
bridge: SyncTransport
rfq: SyncTransport
clob: SyncTransport
_resolved_environment_config: InitVar[EnvironmentConfig | None] = None
Expand Down Expand Up @@ -54,6 +55,7 @@ class AsyncClientContext:
environment_config: EnvironmentConfig = field(init=False, repr=False)
gamma: AsyncTransport
data: AsyncTransport
bridge: AsyncTransport
rfq: AsyncTransport
clob: AsyncTransport
perps: AsyncTransport
Expand Down
103 changes: 99 additions & 4 deletions src/polymarket/clients/async_public.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from polymarket._internal.actions import builders as _builders_actions
from polymarket._internal.actions import clob as _clob_actions
from polymarket._internal.actions import data as _data_actions
from polymarket._internal.actions import funding as _funding_actions
from polymarket._internal.actions import gamma as _gamma_actions
from polymarket._internal.actions import rewards as _rewards_actions
from polymarket._internal.actions import rfq as _rfq_actions
Expand Down Expand Up @@ -57,6 +58,10 @@
ComboMarket,
Comment,
Event,
FundingAddressSet,
FundingAssetCatalog,
FundingQuote,
FundingTransaction,
LastTradePrice,
LastTradePriceForToken,
Market,
Expand Down Expand Up @@ -162,6 +167,7 @@ def __init__(
_resolved_environment_config=config,
gamma=AsyncTransport(base_url=config.gamma_url, logger=logger),
data=AsyncTransport(base_url=config.data_url, logger=logger),
bridge=AsyncTransport(base_url=config.bridge_url, logger=logger),
rfq=AsyncTransport(base_url=config.rfq_url, logger=logger),
clob=AsyncTransport(base_url=config.clob_url, logger=logger),
perps=AsyncTransport(base_url=config.perps_url, logger=logger),
Expand Down Expand Up @@ -364,15 +370,104 @@ async def close(self) -> None:
await self._ctx.data.close()
finally:
try:
await self._ctx.rfq.close()
await self._ctx.bridge.close()
finally:
try:
await self._ctx.clob.close()
await self._ctx.rfq.close()
finally:
try:
await self._ctx.perps.close()
await self._ctx.clob.close()
finally:
await self._rpc.close()
try:
await self._ctx.perps.close()
finally:
await self._rpc.close()

async def create_deposit_addresses(
self, *, wallet: str, builder_code: str | None = None
) -> FundingAddressSet:
"""Create chain-specific deposit addresses for a wallet."""
path, body, headers = _funding_actions.build_create_deposit_addresses_request(
wallet=wallet,
builder_code=builder_code,
)
return _funding_actions.parse_funding_address_set(
await self._ctx.bridge.post_json(path, json=body, headers=headers)
)

async def create_withdrawal_addresses(
self,
*,
wallet: str,
destination_chain_id: int,
destination_token_address: str,
recipient_address: str,
builder_code: str | None = None,
) -> FundingAddressSet:
"""Create addresses for withdrawing to a destination chain and token.

Send pUSD to the returned EVM address to start the withdrawal.
"""
path, body, headers = _funding_actions.build_create_withdrawal_addresses_request(
wallet=wallet,
destination_chain_id=destination_chain_id,
destination_token_address=destination_token_address,
recipient_address=recipient_address,
builder_code=builder_code,
)
return _funding_actions.parse_funding_address_set(
await self._ctx.bridge.post_json(path, json=body, headers=headers)
)

async def fetch_supported_funding_assets(self) -> FundingAssetCatalog:
"""Fetch the chain and token pairs supported for account funding."""
return _funding_actions.parse_funding_asset_catalog(
await self._ctx.bridge.get_json("/supported-assets")
)

async def fetch_funding_quote(
self,
*,
amount: int,
source_chain_id: int,
source_token_address: str,
destination_chain_id: int,
destination_token_address: str,
recipient_address: str,
) -> FundingQuote:
"""Estimate a transfer between supported funding assets."""
path, body = _funding_actions.build_funding_quote_request(
amount=amount,
source_chain_id=source_chain_id,
source_token_address=source_token_address,
destination_chain_id=destination_chain_id,
destination_token_address=destination_token_address,
recipient_address=recipient_address,
)
return _funding_actions.parse_funding_quote(
await self._ctx.bridge.post_json(path, json=body)
)

def list_funding_transactions(
self, *, address: str, page_size: int = 50
) -> AsyncPaginator[FundingTransaction]:
"""List deposit and withdrawal transactions for a bridge address, newest first.

Returns:
An async paginator over transaction-status records.
"""

async def fetch(cursor: str | None) -> Page[FundingTransaction]:
path, params = _funding_actions.build_list_funding_transactions_request(
address=address,
page_size=page_size,
cursor=cursor,
)
return _funding_actions.parse_funding_transactions_page(
await self._ctx.bridge.get_json(path, params=params)
)

return AsyncPaginator(fetch=fetch)

@overload
async def get_market(
Expand Down
Loading
Loading