Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/sell #350

Open
wants to merge 35 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d841608
feat/add sell outcome token feature
annasambrook Nov 7, 2024
501b33d
fix error in reported message
annasambrook Nov 12, 2024
65cb73a
add return amount and sell outcome token round
annasambrook Nov 13, 2024
b7e7a51
chore: generators
annasambrook Nov 13, 2024
a50ecad
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/sell
annasambrook Nov 13, 2024
1284dfb
chore: generators
annasambrook Nov 13, 2024
7cf0997
make requested changes
annasambrook Nov 15, 2024
4188687
Merge remote-tracking branch 'refs/remotes/origin/hotfix/staking-kpi-…
annasambrook Nov 22, 2024
104de4f
make suggested changes
annasambrook Nov 22, 2024
e35ce11
chore: generators
annasambrook Nov 22, 2024
1711625
fix: change copyright date
annasambrook Nov 22, 2024
502897e
chore: generators
annasambrook Nov 22, 2024
32fb70d
Merge branch 'refs/heads/hotfix/staking-kpi-reachability' into feat/sell
annasambrook Dec 5, 2024
445998e
add: previous_vote and sell logic
annasambrook Dec 5, 2024
fe78d74
add: initial fsm changes to include sell outcome token round
annasambrook Dec 5, 2024
3fd007f
chore: generators
annasambrook Dec 5, 2024
5e0524c
fix: copyright date
annasambrook Dec 6, 2024
6f47bf1
Merge branch 'hotfix/staking-kpi-reachability' into feat/sell
Adamantios Dec 6, 2024
b65ba87
chore: ignore bandit's false positive error
Adamantios Dec 6, 2024
e9ba6ee
chore: run generators
Adamantios Dec 6, 2024
a3aae14
feat: update the bets so that we store investments per vote
Adamantios Dec 6, 2024
89cd038
chore: create an action to enforce a merge rule for `main`
Adamantios Dec 6, 2024
cf12051
Merge pull request #367 from valory-xyz/feat/bets-structure
Adamantios Dec 9, 2024
3dc7f7e
Merge remote-tracking branch 'origin/feat/sell' into feat/sell
annasambrook Dec 9, 2024
2709c8c
fix: approval tx status
annasambrook Dec 9, 2024
f919d2e
fix: build approval tx
annasambrook Dec 9, 2024
0a5fc2c
fix: make return amount property
annasambrook Dec 9, 2024
d0e3497
fix: build approval tx token
annasambrook Dec 9, 2024
2f0afa2
fix: remove is wxdai check
annasambrook Dec 9, 2024
87d1205
chore: generators
annasambrook Dec 9, 2024
79191b9
Merge branch 'refs/heads/hotfix/staking-kpi-reachability' into feat/sell
annasambrook Dec 9, 2024
2e90b9e
chore: generators
annasambrook Dec 9, 2024
cdfa138
chore: generators
annasambrook Dec 9, 2024
d234b5e
Update packages/valory/skills/decision_maker_abci/behaviours/bet_plac…
annasambrook Dec 9, 2024
235881b
Update packages/valory/skills/decision_maker_abci/behaviours/bet_plac…
annasambrook Dec 9, 2024
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
53 changes: 52 additions & 1 deletion packages/valory/contracts/market_maker/contract.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2023 Valory AG
# Copyright 2024 Valory AG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -133,3 +133,54 @@ def get_buy_data(
outcomeIndex=outcome_index,
minOutcomeTokensToBuy=min_outcome_tokens_to_buy,
)

@classmethod
def calc_sell_amount(
cls,
ledger_api: EthereumApi,
contract_address: str,
return_amount: int,
outcome_index: int,
) -> JSONLike:
"""
Calculate the buy amount.

:param ledger_api: the ledger API object
:param contract_address: the contract address
:param return_amount: the amount the user will have returned
:param outcome_index: the index of the answer's outcome that the user wants to sell for
:return: the outcomeTokenSellAmount
"""
outcome_token_sell_amount = cls._method_call(
ledger_api,
contract_address,
"calcSellAmount",
returnAmount=return_amount,
outcomeIndex=outcome_index,
)
return dict(outcomeTokenSellAmount=outcome_token_sell_amount)

def get_sell_data(
cls,
ledger_api: LedgerApi,
contract_address: str,
return_amount: int,
outcome_index: int,
max_outcome_tokens_to_sell: int,
) -> Dict[str, bytes]:
"""Gets the encoded arguments for a sell tx, which should only be called via the multisig.

:param ledger_api: the ledger API object
:param contract_address: the contract address
:param return_amount: the amount the user have returned
:param outcome_index: the index of the answer's outcome that the user wants to sell tokens for
:param max_outcome_tokens_to_sell: the output of the `calcSellAmount` contract method
"""
return cls._encode_abi(
ledger_api,
contract_address,
"sell",
returnAmount=return_amount,
outcomeIndex=outcome_index,
maxOutcomeTokenSellAmount=max_outcome_tokens_to_sell,
)
2 changes: 1 addition & 1 deletion packages/valory/contracts/market_maker/contract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fingerprint:
README.md: bafybeiegnihrovfkk5big52pl4bo6evt5toqvvmft2jgnq6ofdbhfp7xwa
__init__.py: bafybeicoucixii3fv5xlpk3zfewm4ys4okidcng54bhtjxvwup7g2jcjza
build/FixedProductMarketMaker.json: bafybeigim7n3f67r5czfc5wp2m7cxzxwvnhxops3n5j2zlawenan7qrrtu
contract.py: bafybeicfiicuke4ly5hpcxtyvnob5bgzrqsopuxalruxp2m3w4koxmhrqa
contract.py: bafybeifcopgnnts3dbfh636vvdsgporlkt2olwr2iwuwaom5vjcrtbe5wi
fingerprint_ignore_patterns: []
contracts: []
class_name: FixedProductMarketMakerContract
Expand Down
30 changes: 30 additions & 0 deletions packages/valory/skills/decision_maker_abci/behaviours/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from aea.configurations.data_types import PublicId
from aea.protocols.base import Message
from aea.protocols.dialogue.base import Dialogue
from hexbytes import HexBytes

from packages.valory.contracts.erc20.contract import ERC20
from packages.valory.contracts.gnosis_safe.contract import (
Expand Down Expand Up @@ -724,6 +725,35 @@ def finish_behaviour(self, payload: BaseTxPayload) -> Generator:

self.set_done()

def build_approval_tx(
self, amount: int, market_maker_contract_address: str, to_address: str
) -> WaitableConditionType:
"""Build an ERC20 approve transaction."""
response_msg = yield from self.get_contract_api_response(
performative=ContractApiMessage.Performative.GET_STATE, # type: ignore
contract_address=self.collateral_token,
contract_id=str(ERC20.contract_id),
contract_callable="build_approval_tx",
spender=market_maker_contract_address,
amount=amount,
)

if response_msg.performative != ContractApiMessage.Performative.STATE:
self.context.logger.info(f"Could not build approval tx: {response_msg}")
return False

approval_data = response_msg.state.body.get("data")
if approval_data is None:
self.context.logger.info(f"Could not build approval tx: {response_msg}")
return False

batch = MultisendBatch(
to=to_address,
Adamantios marked this conversation as resolved.
Show resolved Hide resolved
data=HexBytes(approval_data),
)
self.multisend_batches.append(batch)
return True


class BaseSubscriptionBehaviour(DecisionMakerBaseBehaviour, ABC):
"""Base class for subscription behaviours."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,30 +99,12 @@ def _build_exchange_tx(self) -> WaitableConditionType:

def _build_approval_tx(self) -> WaitableConditionType:
"""Build an ERC20 approve transaction."""
response_msg = yield from self.get_contract_api_response(
performative=ContractApiMessage.Performative.GET_STATE, # type: ignore
contract_address=self.collateral_token,
contract_id=str(ERC20.contract_id),
contract_callable="build_approval_tx",
spender=self.market_maker_contract_address,
amount=self.investment_amount,
approval_tx = yield from self.build_approval_tx(
self.investment_amount,
self.market_maker_contract_address,
self.collateral_token,
annasambrook marked this conversation as resolved.
Show resolved Hide resolved
)

if response_msg.performative != ContractApiMessage.Performative.STATE:
self.context.logger.info(f"Could not build approval tx: {response_msg}")
return False

approval_data = response_msg.state.body.get("data")
if approval_data is None:
self.context.logger.info(f"Could not build approval tx: {response_msg}")
return False

batch = MultisendBatch(
to=self.collateral_token,
data=HexBytes(approval_data),
)
self.multisend_batches.append(batch)
return True
return approval_tx
annasambrook marked this conversation as resolved.
Show resolved Hide resolved

def _calc_buy_amount(self) -> WaitableConditionType:
"""Calculate the buy amount of the conditional token."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2024 Valory AG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ------------------------------------------------------------------------------


"""This module contains the behaviour for selling a token."""
from typing import Any, Generator, Optional, cast

from hexbytes import HexBytes

from packages.valory.contracts.market_maker.contract import (
FixedProductMarketMakerContract,
)
from packages.valory.protocols.contract_api import ContractApiMessage
from packages.valory.skills.decision_maker_abci.behaviours.base import (
DecisionMakerBaseBehaviour,
WaitableConditionType,
)
from packages.valory.skills.decision_maker_abci.models import MultisendBatch
from packages.valory.skills.decision_maker_abci.payloads import MultisigTxPayload
from packages.valory.skills.decision_maker_abci.states.sell_outcome_token import (
SellOutcomeTokenRound,
)


class SellTokenBehaviour(DecisionMakerBaseBehaviour):
"""A behaviour in which the agents sell a token."""

matching_round = SellOutcomeTokenRound

def __init__(self, **kwargs: Any) -> None:
"""Initialize the sell token behaviour."""
super().__init__(**kwargs)
self.sell_amount: float = 0.0
self.return_amount: int = 0

@property
def market_maker_contract_address(self) -> str:
"""Get the contract address of the market maker on which the service is going to place the bet."""
return self.sampled_bet.id

@property
def outcome_index(self) -> int:
"""Get the index of the outcome for which the service is going to sell token."""
return cast(int, self.synchronized_data.vote)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move these to the base from the bet placement behaviour too, as you did with the approval tx.


def _build_approval_tx(self) -> WaitableConditionType:
"""Build an ERC20 approve transaction."""
approval_tx = yield from self.build_approval_tx(
self.return_amount,
self.market_maker_contract_address,
self.market_maker_contract_address,
Adamantios marked this conversation as resolved.
Show resolved Hide resolved
)
return approval_tx
Adamantios marked this conversation as resolved.
Show resolved Hide resolved

def _calc_sell_amount(self) -> WaitableConditionType:
"""Calculate the sell amount of the conditional token."""
response_msg = yield from self.get_contract_api_response(
performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, # type: ignore
contract_address=self.market_maker_contract_address,
contract_id=str(FixedProductMarketMakerContract.contract_id),
contract_callable="calc_sell_amount",
return_amount=self.return_amount,
outcome_index=self.outcome_index,
)
if response_msg.performative != ContractApiMessage.Performative.RAW_TRANSACTION:
self.context.logger.error(
f"Could not calculate the sell amount: {response_msg}"
)
return False

sell_amount = response_msg.raw_transaction.body.get(
"outcomeTokenSellAmount", None
)
if sell_amount is None:
self.context.logger.error(
f"Something went wrong while trying to get the outcomeTokenSellAmount amount for the conditional token: {response_msg}"
)
return False

self.sell_amount = sell_amount
return True
Comment on lines +82 to +108
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has a lot of code dup with the calc buy method, therefore we could create a generic calc amount method with a flag buy and calc the buy or sell amount based on the flag.


def _build_sell_tx(self) -> WaitableConditionType:
"""Get the sell tx data encoded."""
response_msg = yield from self.get_contract_api_response(
performative=ContractApiMessage.Performative.GET_STATE, # type: ignore
contract_address=self.market_maker_contract_address,
contract_id=str(FixedProductMarketMakerContract.contract_id),
contract_callable="get_sell_data",
return_amount=self.return_amount,
outcome_index=self.outcome_index,
max_outcome_tokens_to_sell=self.sell_amount,
)
if response_msg.performative != ContractApiMessage.Performative.STATE:
self.context.logger.error(
f"Could not get the data for the buy transaction: {response_msg}"
)
return False

sell_data = response_msg.state.body.get("data", None)
if sell_data is None:
self.context.logger.error(
f"Something went wrong while trying to encode the buy data: {response_msg}"
)
return False

batch = MultisendBatch(
to=self.market_maker_contract_address,
data=HexBytes(sell_data),
)
self.multisend_batches.append(batch)
return True
Comment on lines +110 to +139
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.


def _prepare_safe_tx(self) -> Generator[None, None, Optional[str]]:
"""Prepare the safe transaction for selling an outcome token and return the hex for the tx settlement skill."""
for step in (
self._build_approval_tx,
self._calc_sell_amount,
self._build_sell_tx,
self._build_multisend_data,
self._build_multisend_safe_tx_hash,
):
yield from self.wait_for_condition_with_sleep(step)

outcome = self.sampled_bet.get_outcome(self.outcome_index)
investment = self._collateral_amount_info(self.return_amount)
self.context.logger.info(
f"Preparing a multisig transaction to sell the outcome token for {outcome!r}, with confidence "
f"{self.synchronized_data.confidence!r}, for the amount of {investment}, which is equal to the amount of "
f"{self.sell_amount!r} WEI of the conditional token corresponding to {outcome!r}."
)

return self.tx_hex

def async_act(self) -> Generator:
"""Do the action."""

agent = self.context.agent_address

with self.context.benchmark_tool.measure(self.behaviour_id).local():
tx_submitter = betting_tx_hex = mocking_mode = None

# if the vote is the same as the previous vote then there is no change in the supported outcome, so we
# should not sell
if self.synchronized_data.vote == self.synchronized_data.previous_vote:
payload = MultisigTxPayload(
agent, tx_submitter, betting_tx_hex, mocking_mode
)

yield from self.finish_behaviour(payload)
Comment on lines +170 to +177
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will not need this check as it will be moved to the post tx settlement that comes after the bet placement round.


self.return_amount = self.bets[
self.synchronized_data.sampled_bet_index
].invested_amount
Adamantios marked this conversation as resolved.
Show resolved Hide resolved

if self.is_wxdai:
tx_submitter = self.matching_round.auto_round_id()
betting_tx_hex = yield from self._prepare_safe_tx()
Adamantios marked this conversation as resolved.
Show resolved Hide resolved

payload = MultisigTxPayload(
agent, tx_submitter, betting_tx_hex, mocking_mode
)

yield from self.finish_behaviour(payload)
6 changes: 6 additions & 0 deletions packages/valory/skills/decision_maker_abci/states/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ def vote(self) -> Optional[int]:
vote = self.db.get_strict("vote")
return int(vote) if vote is not None else None

@property
def previous_vote(self) -> Optional[int]:
"""Get the bet's previous vote index."""
previous_vote = self.db.get_strict("previous_vote")
return int(previous_vote) if previous_vote is not None else None

@property
def confidence(self) -> float:
"""Get the vote's confidence."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class DecisionReceiveRound(CollectSameUntilThresholdRound):

def end_block(self) -> Optional[Tuple[SynchronizedData, Enum]]:
"""Process the end of the block."""

# update the previous vote before calling the super method
SynchronizedData.update(
previous_vote=SynchronizedData.vote
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need to update this as we discussed.


res = super().end_block()
if res is None:
return None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2021-2024 Valory AG
annasambrook marked this conversation as resolved.
Show resolved Hide resolved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ------------------------------------------------------------------------------


"""This module contains the sell outcome token state of the decision-making abci app."""

from packages.valory.skills.decision_maker_abci.states.base import TxPreparationRound


class SellOutcomeTokenRound(TxPreparationRound):
"""A round for selling a token."""
2 changes: 1 addition & 1 deletion packages/valory/skills/market_manager_abci/bets.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decoder will have to updated to check for new missing arguments

Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class Bet:
prediction_response: PredictionResponse = dataclasses.field(
default_factory=get_default_prediction_response
)
invested_amount: float = 0.0
invested_amount: int = 0
position_liquidity: int = 0
potential_net_profit: int = 0
processed_timestamp: int = 0
Expand Down
Loading