Skip to content
Open
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
187 changes: 187 additions & 0 deletions src/polymarket/_internal/actions/relayer/approvals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import cast

from polymarket._internal.actions.relayer.calls import (
MAX_UINT256,
TransactionCall,
decode_erc20_allowance_result,
decode_erc1155_is_approved_for_all_result,
erc20_allowance_call,
erc20_approval_call,
erc1155_is_approved_for_all_call,
erc1155_set_approval_for_all_call,
)
from polymarket._internal.eoa.rpc import JsonRpcClient, SyncJsonRpcClient
from polymarket.environments import Environment
from polymarket.types import EvmAddress


@dataclass(frozen=True, slots=True)
class _Erc20TradingApproval:
token_address: EvmAddress
spender: EvmAddress
amount: int


@dataclass(frozen=True, slots=True)
class _Erc1155TradingApproval:
token_address: EvmAddress
operator: EvmAddress


async def resolve_missing_trading_approval_calls(
rpc: JsonRpcClient, *, wallet: EvmAddress, environment: Environment
) -> list[TransactionCall]:
erc20, erc1155 = _required_trading_approvals(environment)
erc20_missing: list[TransactionCall] = []
for approval in erc20:
check = erc20_allowance_call(
token_address=approval.token_address,
owner=wallet,
spender=approval.spender,
)
allowance = decode_erc20_allowance_result(
await rpc.eth_call(to=str(check.to), data=check.data)
)
if allowance < approval.amount:
erc20_missing.append(
erc20_approval_call(
token_address=approval.token_address,
spender=approval.spender,
amount=approval.amount,
)
)

erc1155_missing: list[TransactionCall] = []
for approval in erc1155:
check = erc1155_is_approved_for_all_call(
token_address=approval.token_address,
owner=wallet,
operator=approval.operator,
)
approved = decode_erc1155_is_approved_for_all_result(
await rpc.eth_call(to=str(check.to), data=check.data)
)
if not approved:
erc1155_missing.append(
erc1155_set_approval_for_all_call(
token_address=approval.token_address,
operator=approval.operator,
approved=True,
)
)

return erc20_missing + erc1155_missing


def resolve_missing_trading_approval_calls_sync(
rpc: SyncJsonRpcClient, *, wallet: EvmAddress, environment: Environment
) -> list[TransactionCall]:
erc20, erc1155 = _required_trading_approvals(environment)
erc20_missing: list[TransactionCall] = []
for approval in erc20:
check = erc20_allowance_call(
token_address=approval.token_address,
owner=wallet,
spender=approval.spender,
)
allowance = decode_erc20_allowance_result(rpc.eth_call(to=str(check.to), data=check.data))
if allowance < approval.amount:
erc20_missing.append(
erc20_approval_call(
token_address=approval.token_address,
spender=approval.spender,
amount=approval.amount,
)
)

erc1155_missing: list[TransactionCall] = []
for approval in erc1155:
check = erc1155_is_approved_for_all_call(
token_address=approval.token_address,
owner=wallet,
operator=approval.operator,
)
approved = decode_erc1155_is_approved_for_all_result(
rpc.eth_call(to=str(check.to), data=check.data)
)
if not approved:
erc1155_missing.append(
erc1155_set_approval_for_all_call(
token_address=approval.token_address,
operator=approval.operator,
approved=True,
)
)

return erc20_missing + erc1155_missing


def _required_trading_approvals(
environment: Environment,
) -> tuple[list[_Erc20TradingApproval], list[_Erc1155TradingApproval]]:
collateral = cast(EvmAddress, environment.collateral_token)
conditional = cast(EvmAddress, environment.conditional_tokens)
return (
[
_Erc20TradingApproval(
token_address=collateral,
spender=cast(EvmAddress, environment.standard_exchange),
amount=MAX_UINT256,
),
_Erc20TradingApproval(
token_address=collateral,
spender=cast(EvmAddress, environment.neg_risk_exchange),
amount=MAX_UINT256,
),
_Erc20TradingApproval(
token_address=collateral,
spender=cast(EvmAddress, environment.neg_risk_adapter),
amount=MAX_UINT256,
),
_Erc20TradingApproval(
token_address=collateral,
spender=cast(EvmAddress, environment.collateral_adapter),
amount=MAX_UINT256,
),
_Erc20TradingApproval(
token_address=collateral,
spender=cast(EvmAddress, environment.neg_risk_collateral_adapter),
amount=MAX_UINT256,
),
],
[
_Erc1155TradingApproval(
token_address=conditional,
operator=cast(EvmAddress, environment.standard_exchange),
),
_Erc1155TradingApproval(
token_address=conditional,
operator=cast(EvmAddress, environment.neg_risk_exchange),
),
_Erc1155TradingApproval(
token_address=conditional,
operator=cast(EvmAddress, environment.neg_risk_adapter),
),
_Erc1155TradingApproval(
token_address=conditional,
operator=cast(EvmAddress, environment.collateral_adapter),
),
_Erc1155TradingApproval(
token_address=conditional,
operator=cast(EvmAddress, environment.neg_risk_collateral_adapter),
),
_Erc1155TradingApproval(
token_address=conditional,
operator=cast(EvmAddress, environment.auto_redeem_operator),
),
],
)


__all__ = [
"resolve_missing_trading_approval_calls",
"resolve_missing_trading_approval_calls_sync",
]
47 changes: 46 additions & 1 deletion src/polymarket/_internal/actions/relayer/calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from dataclasses import dataclass
from typing import cast

from eth_abi.abi import decode as abi_decode
from eth_abi.abi import encode as abi_encode
from eth_utils.crypto import keccak
from eth_utils.hexadecimal import decode_hex

from polymarket.errors import UserInputError
from polymarket.errors import UnexpectedResponseError, UserInputError
from polymarket.types import EvmAddress, HexString

MAX_UINT256 = (1 << 256) - 1
Expand All @@ -18,8 +19,10 @@ def _selector(signature: str) -> bytes:


_ERC20_APPROVE_SELECTOR = _selector("approve(address,uint256)")
_ERC20_ALLOWANCE_SELECTOR = _selector("allowance(address,address)")
_ERC20_TRANSFER_SELECTOR = _selector("transfer(address,uint256)")
_ERC1155_SET_APPROVAL_FOR_ALL_SELECTOR = _selector("setApprovalForAll(address,bool)")
_ERC1155_IS_APPROVED_FOR_ALL_SELECTOR = _selector("isApprovedForAll(address,address)")
_CTF_SPLIT_POSITION_SELECTOR = _selector("splitPosition(address,bytes32,bytes32,uint256[],uint256)")
_CTF_MERGE_POSITIONS_SELECTOR = _selector(
"mergePositions(address,bytes32,bytes32,uint256[],uint256)"
Expand Down Expand Up @@ -52,6 +55,19 @@ def erc20_approval_call(
)


def erc20_allowance_call(
*, token_address: EvmAddress, owner: EvmAddress, spender: EvmAddress
) -> TransactionCall:
payload = _ERC20_ALLOWANCE_SELECTOR + abi_encode(
["address", "address"], [str(owner), str(spender)]
)
return TransactionCall(to=token_address, data=cast(HexString, "0x" + payload.hex()))


def decode_erc20_allowance_result(data: str) -> int:
return cast(int, _decode_return_data(data, "uint256"))


def erc20_transfer_call(
*, token_address: EvmAddress, recipient: EvmAddress, amount: int
) -> TransactionCall:
Expand All @@ -77,6 +93,19 @@ def erc1155_set_approval_for_all_call(
)


def erc1155_is_approved_for_all_call(
*, token_address: EvmAddress, owner: EvmAddress, operator: EvmAddress
) -> TransactionCall:
payload = _ERC1155_IS_APPROVED_FOR_ALL_SELECTOR + abi_encode(
["address", "address"], [str(owner), str(operator)]
)
return TransactionCall(to=token_address, data=cast(HexString, "0x" + payload.hex()))


def decode_erc1155_is_approved_for_all_result(data: str) -> bool:
return cast(bool, _decode_return_data(data, "bool"))


def split_position_call(
*,
target: EvmAddress,
Expand Down Expand Up @@ -157,6 +186,18 @@ def _condition_id_bytes(condition_id: str) -> bytes:
return raw


def _decode_return_data(data: str, abi_type: str) -> object:
try:
values = abi_decode([abi_type], decode_hex(data))
except Exception as error:
raise UnexpectedResponseError(
f"Could not decode {abi_type} return data: {error}"
) from error
if len(values) != 1:
raise UnexpectedResponseError(f"Expected one {abi_type} return value, got {len(values)}")
return values[0]


def encode_proxy_call(calls: list[TransactionCall]) -> HexString:
if not calls:
raise UserInputError("encode_proxy_call requires at least one call")
Expand Down Expand Up @@ -192,9 +233,13 @@ def encode_safe_multisend_call(calls: list[TransactionCall]) -> HexString:
"MAX_UINT256",
"TransactionCall",
"ctf_redeem_positions_call",
"decode_erc1155_is_approved_for_all_result",
"decode_erc20_allowance_result",
"encode_proxy_call",
"encode_safe_multisend_call",
"erc1155_is_approved_for_all_call",
"erc1155_set_approval_for_all_call",
"erc20_allowance_call",
"erc20_approval_call",
"erc20_transfer_call",
"merge_positions_call",
Expand Down
4 changes: 2 additions & 2 deletions src/polymarket/clients/async_public.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
MarketSpec,
PublicSubscription,
SportsSpec,
_normalize_specs,
normalize_specs,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -192,7 +192,7 @@ async def subscribe(
A subscription handle. Iterate over it to receive events and close it
when finished.
"""
items = _normalize_specs(specs)
items = normalize_specs(specs)
# AsyncSubscriptionHandle is invariant in T, so per-channel handles
# can't widen to the union type at the type level. Cast at the
# boundary — the underlying queue holds whatever was pushed into it.
Expand Down
Loading
Loading