Skip to content

Commit

Permalink
496 seer market creation on pmat (#606)
Browse files Browse the repository at this point in the history
* WIP

* Added test for create Seer market

* Fixed Seer subgraph invocations

* Bumped version

* Added script for creating seer markets

* Script working

* Fixed isort

* Tried to fix tests

* Adding more logs to debug test

* Fetching market from event logs

* Adding PR comments

* Trying magic for fixing tests

* Trying to make test pass

* Trying to make test pass (2)

* Sending to a differente agent

* Trying test again

* Contract reverted to old state

* Removed optional fields
  • Loading branch information
gabrielfior authored Feb 7, 2025
1 parent 38443ef commit 231c5c3
Show file tree
Hide file tree
Showing 12 changed files with 1,327 additions and 418 deletions.
758 changes: 373 additions & 385 deletions poetry.lock

Large diffs are not rendered by default.

609 changes: 609 additions & 0 deletions prediction_market_agent_tooling/abis/seer_market_factory.abi.json

Large diffs are not rendered by default.

43 changes: 41 additions & 2 deletions prediction_market_agent_tooling/markets/seer/data_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import typing as t

from eth_typing import HexAddress
from pydantic import BaseModel, ConfigDict, Field
from web3.constants import ADDRESS_ZERO

from prediction_market_agent_tooling.gtypes import HexBytes, Wei


class CreateCategoricalMarketsParams(BaseModel):
model_config = ConfigDict(populate_by_name=True)

from prediction_market_agent_tooling.gtypes import HexBytes
market_name: str = Field(..., alias="marketName")
outcomes: list[str]
# Only relevant for scalar markets
question_start: str = Field(alias="questionStart", default="")
question_end: str = Field(alias="questionEnd", default="")
outcome_type: str = Field(alias="outcomeType", default="")

# Not needed for non-conditional markets.
parent_outcome: int = Field(alias="parentOutcome", default=0)
parent_market: HexAddress = Field(alias="parentMarket", default=ADDRESS_ZERO)

category: str
lang: str
lower_bound: int = Field(alias="lowerBound", default=0)
upper_bound: int = Field(alias="upperBound", default=0)
min_bond: Wei = Field(..., alias="minBond")
opening_time: int = Field(..., alias="openingTime")
token_names: list[str] = Field(..., alias="tokenNames")


class SeerParentMarket(BaseModel):
Expand All @@ -13,8 +40,11 @@ class SeerMarket(BaseModel):
id: HexBytes
title: str = Field(alias="marketName")
outcomes: list[str]
parent_market: SeerParentMarket | None = Field(alias="parentMarket")
wrapped_tokens: list[HexBytes] = Field(alias="wrappedTokens")
parent_outcome: int = Field(alias="parentOutcome")
parent_market: t.Optional[SeerParentMarket] = Field(
alias="parentMarket", default=None
)


class SeerToken(BaseModel):
Expand All @@ -29,3 +59,12 @@ class SeerPool(BaseModel):
liquidity: int
token0: SeerToken
token1: SeerToken


class NewMarketEvent(BaseModel):
market: HexAddress
marketName: str
parentMarket: HexAddress
conditionId: HexBytes
questionId: HexBytes
questionsIds: list[HexBytes]
92 changes: 92 additions & 0 deletions prediction_market_agent_tooling/markets/seer/seer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from eth_typing import ChecksumAddress
from web3 import Web3
from web3.types import TxReceipt

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.gtypes import xDai
from prediction_market_agent_tooling.markets.seer.data_models import NewMarketEvent
from prediction_market_agent_tooling.markets.seer.seer_contracts import (
SeerMarketFactory,
)
from prediction_market_agent_tooling.tools.contract import (
auto_deposit_collateral_token,
init_collateral_token_contract,
to_gnosis_chain_contract,
)
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
from prediction_market_agent_tooling.tools.web3_utils import xdai_to_wei


def seer_create_market_tx(
api_keys: APIKeys,
initial_funds: xDai,
question: str,
opening_time: DatetimeUTC,
language: str,
outcomes: list[str],
auto_deposit: bool,
category: str,
min_bond_xdai: xDai,
web3: Web3 | None = None,
) -> ChecksumAddress:
web3 = web3 or SeerMarketFactory.get_web3() # Default to Gnosis web3.
initial_funds_wei = xdai_to_wei(initial_funds)

factory_contract = SeerMarketFactory()
collateral_token_address = factory_contract.collateral_token(web3=web3)
collateral_token_contract = to_gnosis_chain_contract(
init_collateral_token_contract(collateral_token_address, web3)
)

if auto_deposit:
auto_deposit_collateral_token(
collateral_token_contract=collateral_token_contract,
api_keys=api_keys,
amount_wei=initial_funds_wei,
web3=web3,
)

# In case of ERC4626, obtained (for example) sDai out of xDai could be lower than the `amount_wei`, so we need to handle it.
initial_funds_in_shares = collateral_token_contract.get_in_shares(
amount=initial_funds_wei, web3=web3
)

# Approve the market maker to withdraw our collateral token.
collateral_token_contract.approve(
api_keys=api_keys,
for_address=factory_contract.address,
amount_wei=initial_funds_in_shares,
web3=web3,
)

# Create the market.
params = factory_contract.build_market_params(
market_question=question,
outcomes=outcomes,
opening_time=opening_time,
language=language,
category=category,
min_bond_xdai=min_bond_xdai,
)
tx_receipt = factory_contract.create_categorical_market(
api_keys=api_keys, params=params, web3=web3
)

# ToDo - Add liquidity to market on Swapr (https://github.com/gnosis/prediction-market-agent-tooling/issues/497)
market_address = extract_market_address_from_tx(
factory_contract=factory_contract, tx_receipt=tx_receipt, web3=web3
)
return market_address


def extract_market_address_from_tx(
factory_contract: SeerMarketFactory, tx_receipt: TxReceipt, web3: Web3
) -> ChecksumAddress:
"""We extract the newly created market from the NewMarket event emitted in the transaction."""
event_logs = (
factory_contract.get_web3_contract(web3=web3)
.events.NewMarket()
.process_receipt(tx_receipt)
)
new_market_event = NewMarketEvent(**event_logs[0]["args"])
return Web3.to_checksum_address(new_market_event.market)
76 changes: 76 additions & 0 deletions prediction_market_agent_tooling/markets/seer/seer_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os

from web3 import Web3
from web3.types import TxReceipt

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.gtypes import ABI, ChecksumAddress, xDai
from prediction_market_agent_tooling.markets.seer.data_models import (
CreateCategoricalMarketsParams,
)
from prediction_market_agent_tooling.tools.contract import (
ContractOnGnosisChain,
abi_field_validator,
)
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
from prediction_market_agent_tooling.tools.web3_utils import xdai_to_wei


class SeerMarketFactory(ContractOnGnosisChain):
# https://gnosisscan.io/address/0x83183da839ce8228e31ae41222ead9edbb5cdcf1#code.
abi: ABI = abi_field_validator(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
"../../abis/seer_market_factory.abi.json",
)
)
address: ChecksumAddress = Web3.to_checksum_address(
"0x83183da839ce8228e31ae41222ead9edbb5cdcf1"
)

@staticmethod
def build_market_params(
market_question: str,
outcomes: list[str],
opening_time: DatetimeUTC,
min_bond_xdai: xDai,
language: str = "en_US",
category: str = "misc",
) -> CreateCategoricalMarketsParams:
return CreateCategoricalMarketsParams(
market_name=market_question,
token_names=[
o.upper() for o in outcomes
], # Following usual token names on Seer (YES,NO).
min_bond=xdai_to_wei(min_bond_xdai),
opening_time=int(opening_time.timestamp()),
outcomes=outcomes,
lang=language,
category=category,
)

def market_count(self, web3: Web3 | None = None) -> int:
count: int = self.call("marketCount", web3=web3)
return count

def market_at_index(self, index: int, web3: Web3 | None = None) -> ChecksumAddress:
market_address: str = self.call("markets", function_params=[index], web3=web3)
return Web3.to_checksum_address(market_address)

def collateral_token(self, web3: Web3 | None = None) -> ChecksumAddress:
collateral_token_address: str = self.call("collateralToken", web3=web3)
return Web3.to_checksum_address(collateral_token_address)

def create_categorical_market(
self,
api_keys: APIKeys,
params: CreateCategoricalMarketsParams,
web3: Web3 | None = None,
) -> TxReceipt:
receipt_tx = self.send(
api_keys=api_keys,
function_name="createCategoricalMarket",
function_params=[params.model_dump(by_alias=True)],
web3=web3,
)
return receipt_tx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def _get_fields_for_markets(self, markets_field: FieldPath) -> list[FieldPath]:
markets_field.factory,
markets_field.creator,
markets_field.marketName,
markets_field.parentOutcome,
markets_field.outcomes,
markets_field.parentMarket.id,
markets_field.finalizeTs,
Expand Down Expand Up @@ -126,7 +127,7 @@ def _get_fields_for_pools(self, pools_field: FieldPath) -> list[FieldPath]:
]
return fields

def get_pools_for_market(self, market: SeerMarket) -> list[SeerPool]:
def get_swapr_pools_for_market(self, market: SeerMarket) -> list[SeerPool]:
# We iterate through the wrapped tokens and put them in a where clause so that we hit the subgraph endpoint just once.
wheres = []
for wrapped_token in market.wrapped_tokens:
Expand Down
2 changes: 1 addition & 1 deletion prediction_market_agent_tooling/monitor/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ def monitor_market_outcome_bias(

if len(df) > 0:
st.altair_chart(
alt.layer(open_chart, resolved_chart).interactive(), # type: ignore # Doesn't expect `LayerChart`, but `Chart`, yet it works.
alt.layer(open_chart, resolved_chart).interactive(),
use_container_width=True,
)

Expand Down
12 changes: 4 additions & 8 deletions prediction_market_agent_tooling/monitor/monitor_app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import typing as t
from datetime import date, datetime, timedelta
from datetime import datetime, timedelta

import streamlit as st

Expand Down Expand Up @@ -105,13 +105,9 @@ def monitor_app(
start_time: DatetimeUTC | None = (
DatetimeUTC.from_datetime(
datetime.combine(
t.cast(
# This will be always a date for us, so casting.
date,
st.date_input(
"Start time",
value=utcnow() - timedelta(weeks=settings.PAST_N_WEEKS),
),
st.date_input(
"Start time",
value=utcnow() - timedelta(weeks=settings.PAST_N_WEEKS),
),
datetime.min.time(),
)
Expand Down
68 changes: 68 additions & 0 deletions scripts/create_market_seer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from datetime import datetime

import typer
from web3 import Web3

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.gtypes import private_key_type, xdai_type
from prediction_market_agent_tooling.loggers import logger
from prediction_market_agent_tooling.markets.omen.data_models import (
OMEN_BINARY_MARKET_OUTCOMES,
)
from prediction_market_agent_tooling.markets.seer.seer import seer_create_market_tx
from prediction_market_agent_tooling.tools.utils import DatetimeUTC


def main(
question: str = typer.Option(),
opening_time: datetime = typer.Option(),
category: str = typer.Option(),
initial_funds: str = typer.Option(),
from_private_key: str = typer.Option(),
safe_address: str = typer.Option(None),
min_bond_xdai: int = typer.Option(0.01),
language: str = typer.Option("en_US"),
outcomes: list[str] = typer.Option(OMEN_BINARY_MARKET_OUTCOMES),
auto_deposit: bool = typer.Option(False),
) -> None:
"""
Creates a market on Seer.
Args:
question (str): The question for the market.
opening_time (datetime): The opening time for the market.
category (str): The category of the market.
initial_funds (str): The initial funds for the market.
from_private_key (str): The private key to use for transactions.
safe_address (str, optional): The safe address for transactions. Defaults to None.
min_bond_xdai (int, optional): The minimum bond in xDai. Defaults to 0.01 xDai.
language (str, optional): The language of the market. Defaults to "en".
outcomes (list[str], optional): The outcomes for the market. Defaults to OMEN_BINARY_MARKET_OUTCOMES.
auto_deposit (bool, optional): Whether to automatically deposit funds. Defaults to False.
Returns:
None
"""
safe_address_checksum = (
Web3.to_checksum_address(safe_address) if safe_address else None
)
api_keys = APIKeys(
BET_FROM_PRIVATE_KEY=private_key_type(from_private_key),
SAFE_ADDRESS=safe_address_checksum,
)
market = seer_create_market_tx(
api_keys=api_keys,
initial_funds=xdai_type(initial_funds),
question=question,
opening_time=DatetimeUTC.from_datetime(opening_time),
category=category,
language=language,
outcomes=outcomes,
auto_deposit=auto_deposit,
min_bond_xdai=xdai_type(min_bond_xdai),
)
logger.info(f"Market created: {market}")


if __name__ == "__main__":
typer.run(main)
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def test_get_pools_for_market(handler: SeerSubgraphHandler) -> None:
us_election_market_id = HexBytes("0x43d881f5920ed29fc5cd4917d6817496abbba6d9")
market = handler.get_market_by_id(us_election_market_id)

pools = handler.get_pools_for_market(market)
pools = handler.get_swapr_pools_for_market(market)
assert len(pools) > 1
for pool in pools:
# one of the tokens must be a wrapped token
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from web3 import Web3

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.gtypes import xDai, xdai_type
from prediction_market_agent_tooling.markets.seer.data_models import (
CreateCategoricalMarketsParams,
)
from prediction_market_agent_tooling.markets.seer.seer_contracts import (
SeerMarketFactory,
)
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC


def build_params() -> CreateCategoricalMarketsParams:
return SeerMarketFactory.build_market_params(
market_question="test test test",
outcomes=["Yes", "No"],
opening_time=DatetimeUTC.now(),
language="en_US",
category="misc",
min_bond_xdai=xdai_type(xDai(0.01)),
)


def test_create_market(local_web3: Web3, test_keys: APIKeys) -> None:
factory = SeerMarketFactory()
num_initial_markets = factory.market_count(web3=local_web3)
params = build_params()
factory.create_categorical_market(
api_keys=test_keys, params=params, web3=local_web3
)

num_final_markets = factory.market_count(web3=local_web3)
assert num_initial_markets + 1 == num_final_markets
Loading

0 comments on commit 231c5c3

Please sign in to comment.