diff --git a/packages/packages.json b/packages/packages.json index e9e596eb..5e229c16 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -8,10 +8,10 @@ "contract/valory/merkl_distributor/0.1.0": "bafybeifctofnyhdic2sxmkqujvf3j2wwydhtvzhi6kdeutykenymplf4e4", "contract/valory/staking_token/0.1.0": "bafybeihgp74ojttyzuriukd44biv2ehh4rcc3czm7mv2olw7amepxszbzu", "contract/valory/staking_activity_checker/0.1.0": "bafybeibfqnnqgrchsykidx3x3fgwjmjnml7jyl6e66prhxars3gzgosvxq", - "skill/valory/liquidity_trader_abci/0.1.0": "bafybeihii4e64akqkelii37havxmet3qeg5vlv4vg2toekoaww6tk7zudi", - "skill/valory/optimus_abci/0.1.0": "bafybeiazrizmyz5rbqkodjo3jrregmeoyg2qrov4p5qyh5znggeg2kmtdi", - "agent/valory/optimus/0.1.0": "bafybeicjbstfud6oljhtbobgahpzujyigo2u3eit62srji2ik52irhdv7i", - "service/valory/optimus/0.1.0": "bafybeibjwknk7bchs24irn7ayogp72i2cbaioqcd5dzssqtdq4gihrocu4" + "skill/valory/liquidity_trader_abci/0.1.0": "bafybeiftxq6ipny6wyir3vobayjav7fok5ys5cgwoaewlf6gwoleior364", + "skill/valory/optimus_abci/0.1.0": "bafybeiadjvqfs5is4cf7yb74l2cgmblkvjwfkrv2xn7byhvslmovvwpfzy", + "agent/valory/optimus/0.1.0": "bafybeicjus22cvpmab2jx33x4cnnuhnd2up4ezia7lnr4njbgbqwt6ue7e", + "service/valory/optimus/0.1.0": "bafybeiakhjobjcnqgx2gcdsxhpybwlgcyoll3jovwoygs6hk2hx67cjiiy" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi", diff --git a/packages/valory/agents/optimus/aea-config.yaml b/packages/valory/agents/optimus/aea-config.yaml index e85d67aa..18f589a0 100644 --- a/packages/valory/agents/optimus/aea-config.yaml +++ b/packages/valory/agents/optimus/aea-config.yaml @@ -35,8 +35,8 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeihu2bcgjk2tqjiq2zhk3uogtfszqn4osvdt7ho3fubdpdj4jgdfjm - valory/abstract_round_abci:0.1.0:bafybeibovsktd3uxur45nrcomq5shcn46cgxd5idmhxbmjhg32c5abyqim -- valory/liquidity_trader_abci:0.1.0:bafybeihii4e64akqkelii37havxmet3qeg5vlv4vg2toekoaww6tk7zudi -- valory/optimus_abci:0.1.0:bafybeiazrizmyz5rbqkodjo3jrregmeoyg2qrov4p5qyh5znggeg2kmtdi +- valory/liquidity_trader_abci:0.1.0:bafybeiftxq6ipny6wyir3vobayjav7fok5ys5cgwoaewlf6gwoleior364 +- valory/optimus_abci:0.1.0:bafybeiadjvqfs5is4cf7yb74l2cgmblkvjwfkrv2xn7byhvslmovvwpfzy - valory/registration_abci:0.1.0:bafybeicnth5q4httefsusywx3zrrq4al47owvge72dqf2fziruicq6hqta - valory/reset_pause_abci:0.1.0:bafybeievjciqdvxhqxfjd4whqs27h6qbxqzrae7wwj7fpvxlvmtw3x35im - valory/termination_abci:0.1.0:bafybeid54buqxipiuduw7b6nnliiwsxajnltseuroad53wukfonpxca2om @@ -197,7 +197,7 @@ models: allowed_dexs: ${list:["balancerPool", "UniswapV3"]} initial_assets: ${str:{"ethereum":{"0x0000000000000000000000000000000000000000":"ETH","0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":"USDC"}}} safe_contract_addresses: ${str:{"ethereum":"0x0000000000000000000000000000000000000000","base":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C","optimism":"0x07e27E181Df065141ee90a4DD43cE4113bc9853C"}} - merkl_fetch_campaigns_args: ${str:{"url":"https://api.merkl.xyz/v3/campaigns","creator":"superfest","live":"true"}} + merkl_fetch_campaigns_args: ${str:{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}} allowed_chains: ${list:["optimism","base"]} gas_reserve: ${str:{"ethereum":1000,"optimism":1000,"base":1000}} round_threshold: ${int:0} @@ -216,7 +216,7 @@ models: balancer_vault_contract_addresses: ${str:{"optimism":"0xBA12222222228d8Ba445958a75a0704d566BF2C8","base":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"}} uniswap_position_manager_contract_addresses: ${str:{"optimism":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88","base":"0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1"}} chain_to_chain_key_mapping: ${str:{"ethereum":"eth","optimism":"opt","base":"bas"}} - waiting_period_for_retry: ${int:5} + waiting_period_for_status_check: ${int:5} max_num_of_retries: ${int:5} reward_claiming_time_period: ${int:28800} merkl_distributor_contract_addresses: ${str:{"optimism":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae","base":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae"}} @@ -233,3 +233,4 @@ models: store_path: ${str:/data/} assets_info_filename: ${str:assets.json} pool_info_filename: ${str:current_pool.json} + balancer_graphql_endpoints: ${str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} diff --git a/packages/valory/services/optimus/service.yaml b/packages/valory/services/optimus/service.yaml index d2239028..dcdfbf99 100644 --- a/packages/valory/services/optimus/service.yaml +++ b/packages/valory/services/optimus/service.yaml @@ -6,7 +6,7 @@ aea_version: '>=1.0.0, <2.0.0' license: Apache-2.0 fingerprint: {} fingerprint_ignore_patterns: [] -agent: valory/optimus:0.1.0:bafybeicjbstfud6oljhtbobgahpzujyigo2u3eit62srji2ik52irhdv7i +agent: valory/optimus:0.1.0:bafybeicjus22cvpmab2jx33x4cnnuhnd2up4ezia7lnr4njbgbqwt6ue7e number_of_agents: 1 deployment: {} --- @@ -74,7 +74,8 @@ models: store_path: ${STORE_PATH:str:/data/} assets_info_filename: ${ASSETS_INFO_FILENAME:str:assets.json} pool_info_filename: ${POOL_INFO_FILENAME:str:current_pool.json} - merkl_fetch_campaigns_args: ${MERKL_FETCH_CAMPAIGNS_ARGS:str:{"url":"https://api.merkl.xyz/v3/campaigns","creator":"superfest","live":"true"}} + merkl_fetch_campaigns_args: ${MERKL_FETCH_CAMPAIGNS_ARGS:str:{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}} + balancer_graphql_endpoints: ${BALANCER_GRAPHQL_ENDPOINTS:str:{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}} --- public_id: valory/ledger:0.19.0 type: connection diff --git a/packages/valory/skills/liquidity_trader_abci/behaviours.py b/packages/valory/skills/liquidity_trader_abci/behaviours.py index ccdd5c36..ea9c9513 100644 --- a/packages/valory/skills/liquidity_trader_abci/behaviours.py +++ b/packages/valory/skills/liquidity_trader_abci/behaviours.py @@ -21,11 +21,10 @@ import json import math -import os.path from abc import ABC from collections import defaultdict from enum import Enum -from typing import Any, Dict, Generator, List, Optional, Set, Tuple, Type, Union, cast +from typing import Any, Dict, Generator, List, Optional, Set, Tuple, Type, cast from urllib.parse import urlencode from aea.configurations.data_types import PublicId @@ -97,6 +96,10 @@ # satisfied, and the moment where the checkpoint is called. REQUIRED_REQUESTS_SAFETY_MARGIN = 1 +# type 1 and 2 stand for ERC20 and Concentrated liquidity campaigns respectively +# https://docs.merkl.xyz/integrate-merkl/integrate-merkl-to-your-app#merkl-api +CAMPAIGN_TYPES = ["1", "2"] +INTEGRATOR = "valory" WaitableConditionType = Generator[None, None, Any] @@ -157,7 +160,7 @@ def __init__(self, **kwargs: Any) -> None: self.pools: Dict[str, Any] = {} self.pools[DexTypes.BALANCER.value] = BalancerPoolBehaviour self.pools[DexTypes.UNISWAP_V3.value] = UniswapPoolBehaviour - self.service_staking_state = None + self.service_staking_state = StakingState.UNSTAKED self.strategy = SimpleStrategyBehaviour # Read the assets and current pool self.read_current_pool() @@ -412,6 +415,9 @@ def _calculate_min_num_of_safe_tx_required( ) last_ts_checkpoint = yield from self._get_ts_checkpoint(chain="optimism") + if last_ts_checkpoint is None: + return None + min_num_of_safe_tx_required = ( math.ceil( max(liveness_period, (current_timestamp - last_ts_checkpoint)) @@ -483,25 +489,26 @@ def _get_liveness_period(self, chain: str) -> Generator[None, None, Optional[int def _is_staking_kpi_met(self) -> Generator[None, None, Optional[bool]]: """Return whether the staking KPI has been met (only for staked services).""" - if self.service_staking_state is None: - yield from self._get_service_staking_state(chain="optimism") - if self.service_staking_state != StakingState.STAKED: return False min_num_of_safe_tx_required = self.synchronized_data.min_num_of_safe_tx_required if min_num_of_safe_tx_required is None: - min_num_of_safe_tx_required = yield from self._calculate_min_num_of_safe_tx_required(chain="optimism") + min_num_of_safe_tx_required = ( + yield from self._calculate_min_num_of_safe_tx_required(chain="optimism") + ) if min_num_of_safe_tx_required is None: self.context.logger.error( "Error calculating min number of safe tx required." ) - return None + return False - multisig_nonces_since_last_cp = yield from self._get_multisig_nonces_since_last_cp( + multisig_nonces_since_last_cp = ( + yield from self._get_multisig_nonces_since_last_cp( chain="optimism", multisig=self.params.safe_contract_addresses.get("optimism"), + ) ) if ( multisig_nonces_since_last_cp @@ -523,12 +530,19 @@ def _get_multisig_nonces( chain_id=chain, multisig=multisig, ) + + if multisig_nonces is None or len(multisig_nonces) == 0: + return None + return multisig_nonces[0] def _get_multisig_nonces_since_last_cp( self, chain: str, multisig: str ) -> Generator[None, None, Optional[int]]: multisig_nonces = yield from self._get_multisig_nonces(chain, multisig) + if multisig_nonces is None: + return None + service_info = yield from self._get_service_info(chain) if service_info is None or len(service_info) == 0 or len(service_info[2]) == 0: self.context.logger.error(f"Error fetching service info {service_info}") @@ -540,7 +554,7 @@ def _get_multisig_nonces_since_last_cp( multisig_nonces - multisig_nonces_on_last_checkpoint ) self.context.logger.info( - f"Multisig nonces since last checkpoint: {multisig_nonces_since_last_cp}" + f"Number of safe transactions since last checkpoint: {multisig_nonces_since_last_cp}" ) return multisig_nonces_since_last_cp @@ -611,12 +625,19 @@ def async_act(self) -> Generator: min_num_of_safe_tx_required = None is_staking_kpi_met = False if self.service_staking_state == StakingState.STAKED: - min_num_of_safe_tx_required = yield from self._calculate_min_num_of_safe_tx_required( - chain="optimism" - ) - self.context.logger.info( - f"The minimum number of safe tx required to unlock rewards are {min_num_of_safe_tx_required}" + min_num_of_safe_tx_required = ( + yield from self._calculate_min_num_of_safe_tx_required( + chain="optimism" + ) ) + if min_num_of_safe_tx_required is None: + self.context.logger.error( + "Error calculating min number of safe tx required." + ) + else: + self.context.logger.info( + f"The minimum number of safe tx required to unlock rewards are {min_num_of_safe_tx_required}" + ) is_checkpoint_reached = yield from self._check_if_checkpoint_reached( chain="optimism" ) @@ -630,8 +651,6 @@ def async_act(self) -> Generator: is_staking_kpi_met = False else: is_staking_kpi_met = yield from self._is_staking_kpi_met() - if is_staking_kpi_met is None: - self.service_staking_state = StakingState.UNSTAKED elif self.service_staking_state == StakingState.EVICTED: self.context.logger.error("Service has been evicted!") @@ -736,7 +755,6 @@ def async_act(self) -> Generator: -1 ].round_id ) - is_post_tx_settlement_round = ( last_round_id == PostTxSettlementRound.auto_round_id() and self.synchronized_data.tx_submitter @@ -744,6 +762,7 @@ def async_act(self) -> Generator: ) is_period_threshold_exceeded = ( self.synchronized_data.period_count + - self.synchronized_data.period_number_at_last_cp >= self.params.staking_threshold_period ) @@ -767,21 +786,22 @@ def async_act(self) -> Generator: ) ) - num_of_tx_left_to_meet_kpi = ( - min_num_of_safe_tx_required - multisig_nonces_since_last_cp - ) - if num_of_tx_left_to_meet_kpi > 0: - self.context.logger.info( - f"Number of tx left to meet KPI: {num_of_tx_left_to_meet_kpi}" + if multisig_nonces_since_last_cp and min_num_of_safe_tx_required: + num_of_tx_left_to_meet_kpi = ( + min_num_of_safe_tx_required - multisig_nonces_since_last_cp ) - self.context.logger.info(f"Preparing vanity tx..") - vanity_tx_hex = yield from self._prepare_vanity_tx( - chain="optimism" - ) - self.context.logger.info(f"tx hash: {vanity_tx_hex}") - else: - is_staking_kpi_met = True - self.context.logger.info("KPI met for the day!") + if num_of_tx_left_to_meet_kpi > 0: + self.context.logger.info( + f"Number of tx left to meet KPI: {num_of_tx_left_to_meet_kpi}" + ) + self.context.logger.info(f"Preparing vanity tx..") + vanity_tx_hex = yield from self._prepare_vanity_tx( + chain="optimism" + ) + self.context.logger.info(f"tx hash: {vanity_tx_hex}") + else: + is_staking_kpi_met = True + self.context.logger.info("KPI met for the day!") tx_submitter = self.matching_round.auto_round_id() payload = CheckStakingKPIMetPayload( @@ -872,7 +892,7 @@ class EvaluateStrategyBehaviour(LiquidityTraderBaseBehaviour): def async_act(self) -> Generator: """Async act""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): - yield from self.get_highest_apr_pool() + yield from self.find_highest_apr_pool() actions = [] if self.highest_apr_pool is not None: invest_in_pool = self.strategy.get_decision( @@ -892,59 +912,186 @@ def async_act(self) -> Generator: self.set_done() - def get_highest_apr_pool(self) -> Generator[None, None, None]: - """Get highest APR pool""" - filtered_pools = yield from self._get_filtered_pools() - - if not filtered_pools: - self.context.logger.info("Could not find any eligible pool") - return None + def find_highest_apr_pool(self) -> Generator[None, None, None]: + """Find the pool with the highest APR.""" + all_pools = yield from self._fetch_all_pools() + if not all_pools: + self.context.logger.info("No pools found.") + return - highest_apr = -float("inf") - self.highest_apr_pool = None - - for dex_type, chains in filtered_pools.items(): - for chain, campaigns in chains.items(): - for campaign in campaigns: - apr = campaign.get("apr", 0) - if apr is None: - apr = 0 - if apr > highest_apr: - highest_apr = apr - self.highest_apr_pool = self._extract_pool_info( - dex_type, chain, apr, campaign - ) + eligible_pools = self._filter_eligible_pools(all_pools) + if not eligible_pools: + self.context.logger.info("No eligible pools found.") + return + self.highest_apr_pool = yield from self._determine_highest_apr_pool( + eligible_pools + ) if self.highest_apr_pool: self.context.logger.info(f"Highest APR pool found: {self.highest_apr_pool}") else: self.context.logger.warning("No pools with APR found.") + def _fetch_all_pools(self) -> Generator[None, None, Optional[Dict[str, Any]]]: + """Fetch all pools based on allowed chains.""" + chain_ids = ",".join( + str(self.params.chain_to_chain_id_mapping[chain]) + for chain in self.params.allowed_chains + ) + base_url = self.params.merkl_fetch_campaigns_args.get("url") + creator = self.params.merkl_fetch_campaigns_args.get("creator") + live = self.params.merkl_fetch_campaigns_args.get("live", "true") + + params = { + "chainIds": chain_ids, + "creatorTag": creator, + "live": live, + "types": CAMPAIGN_TYPES, + } + api_url = f"{base_url}?{urlencode(params, doseq=True)}" + self.context.logger.info(f"Fetching campaigns from {api_url}") + + response = yield from self.get_http_response( + method="GET", + url=api_url, + headers={"accept": "application/json"}, + ) + + if response.status_code != 200: + self.context.logger.error( + f"Could not retrieve data from url {api_url}. Status code {response.status_code}. Error Message: {response.body}" + ) + return None + + try: + return json.loads(response.body) + except (ValueError, TypeError) as e: + self.context.logger.error( + f"Could not parse response from api, the following error was encountered {type(e).__name__}: {e}" + ) + return None + + def _filter_eligible_pools(self, all_pools: Dict[str, Any]) -> Dict[str, Any]: + """Filter pools based on allowed assets and LP pools.""" + eligible_pools = defaultdict(lambda: defaultdict(list)) + allowed_dexs = self.params.allowed_dexs + + for chain_id, campaigns in all_pools.items(): + for campaign_list in campaigns.values(): + for campaign in campaign_list.values(): + dex_type = campaign.get("type") or campaign.get("ammName") + if not dex_type or dex_type not in allowed_dexs: + continue + + campaign_apr = campaign.get("apr", 0) + if ( + not campaign_apr + or campaign_apr <= 0 + or campaign_apr <= self.current_pool.get("apr", 0) + ): + continue + + campaign_pool_address = campaign.get("mainParameter") + if ( + not campaign_pool_address + or campaign_pool_address == self.current_pool.get("address") + ): + continue + + chain = next( + ( + k + for k, v in self.params.chain_to_chain_id_mapping.items() + if v == int(chain_id) + ), + None, + ) + eligible_pools[dex_type][chain].append(campaign) + + return eligible_pools + + def _determine_highest_apr_pool( + self, eligible_pools: Dict[str, Any] + ) -> Generator[None, None, Optional[Dict[str, Any]]]: + """Determine the pool with the highest APR from the eligible pools.""" + + highest_apr_pool = None + highest_apr_pool_info = None + while eligible_pools: + highest_apr = -float("inf") + for dex_type, chains in eligible_pools.items(): + for chain, campaigns in chains.items(): + for campaign in campaigns: + apr = campaign.get("apr", 0) or 0 + if apr > highest_apr: + highest_apr = apr + highest_apr_pool_info = (dex_type, chain, campaign) + + if highest_apr_pool_info: + dex_type, chain, campaign = highest_apr_pool_info + highest_apr_pool = yield from self._extract_pool_info( + dex_type, chain, highest_apr, campaign + ) + + # Check the number of tokens for the highest APR pool if it's a Balancer pool + if dex_type == DexTypes.BALANCER.value: + pool_id = highest_apr_pool.get("pool_id") + tokensList = yield from self._fetch_balancer_pool_info( + pool_id, chain, detail="tokensList" + ) + if not tokensList or len(tokensList) != 2: + num_of_tokens = len(tokensList) if tokensList else None + self.context.logger.warning( + f"Balancer pool {pool_id} has {num_of_tokens} tokens, currently we support pools with only 2 tokens" + ) + self.context.logger.info("Searching for another pool") + highest_apr_pool = None + highest_apr_pool_info = None + + # Remove the invalid pool from eligible pools and continue searching + eligible_pools[dex_type][chain].remove(campaign) + if not eligible_pools[dex_type][chain]: + del eligible_pools[dex_type][chain] + + if not eligible_pools[dex_type]: + del eligible_pools[dex_type] + + continue + + return highest_apr_pool + + self.context.logger.warning("No eligible pools found.") + return None + def _extract_pool_info( self, dex_type, chain, apr, campaign - ) -> Optional[Dict[str, Any]]: - """Extract pool info from campaign data""" - # TO-DO: Add support for pools with more than two tokens. - pool_token_dict = {} + ) -> Generator[None, None, Optional[Dict[str, Any]]]: + """Extract pool info from campaign data.""" pool_address = campaign.get("mainParameter") if not pool_address: self.context.logger.error(f"Missing pool address in campaign {campaign}") return None + pool_token_dict = {} + pool_id = None + pool_type = None + if dex_type == DexTypes.BALANCER.value: type_info = campaign.get("typeInfo", {}) + pool_id = type_info.get("poolId") pool_tokens = type_info.get("poolTokens", {}) - # Extracting token0 and token1 with their symbols and addresses pool_token_items = list(pool_tokens.items()) if len(pool_token_items) < 2 or any( - token.get("symbol") is None or address is None - for address, token in pool_token_items + token.get("symbol") is None for _, token in pool_token_items ): self.context.logger.error( f"Invalid pool tokens found in campaign {pool_token_items}" ) return None + pool_type = yield from self._fetch_balancer_pool_info( + pool_id, chain, detail="poolType" + ) pool_token_dict = { "token0": pool_token_items[0][0], "token1": pool_token_items[1][0], @@ -952,14 +1099,14 @@ def _extract_pool_info( "token1_symbol": pool_token_items[1][1].get("symbol"), } - if dex_type == DexTypes.UNISWAP_V3.value: + elif dex_type == DexTypes.UNISWAP_V3.value: pool_info = campaign.get("campaignParameters", {}) if not pool_info: self.context.logger.error( f"No pool tokens info present in campaign {campaign}" ) return None - # Construct the dict for Uniswap V3 tokens with their symbols and addresses + pool_token_dict = { "token0": pool_info.get("token0"), "token1": pool_info.get("token1"), @@ -974,101 +1121,68 @@ def _extract_pool_info( ) return None - pool_data = { + return { "dex_type": dex_type, "chain": chain, "apr": apr, "pool_address": pool_address, + "pool_id": pool_id, + "pool_type": pool_type, + **pool_token_dict, } - pool_data.update(pool_token_dict) - return pool_data - - def _get_filtered_pools(self) -> Generator[None, None, Optional[Dict[str, Any]]]: - """Get filtered pools""" - - filtered_pools = defaultdict(lambda: defaultdict(list)) - - for chain in self.params.allowed_chains: - chain_id = self.params.chain_to_chain_id_mapping.get(chain) - base_url = self.params.merkl_fetch_campaigns_args.get("url") - creator = self.params.merkl_fetch_campaigns_args.get("creator") - live = self.params.merkl_fetch_campaigns_args.get("live", "true") - params = {"chainIds": chain_id, "creatorTag": creator, "live": live} - api_url = f"{base_url}?{urlencode(params)}" - self.context.logger.info(f"Fetching campaigns from {api_url}") + def _fetch_balancer_pool_info( + self, pool_id: str, chain: str, detail: str + ) -> Generator[None, None, Optional[Any]]: + """Fetch the pool type for a Balancer pool using a GraphQL query.""" + + def to_content(query: str) -> bytes: + """Convert the given query string to payload content.""" + finalized_query = {"query": query} + encoded_query = json.dumps(finalized_query, sort_keys=True).encode("utf-8") + return encoded_query + + query = f""" + query {{ + pools(where: {{ id: "{pool_id}" }}) {{ + id + {detail} + }} + }} + """ + + url = self.params.balancer_graphql_endpoints.get(chain) + if not url: + self.context.logger.error(f"No graphql endpoint found for chain {chain}") + return None - response = yield from self.get_http_response( - method="GET", - url=api_url, - headers={"accept": "application/json"}, + response = yield from self.get_http_response( + content=to_content(query), + method="POST", + url=url, + headers={"Content-Type": "application/json"}, + ) + if response.status_code != 200: + self.context.logger.error( + f"Received status code {response.status_code} from the API. Response: {response.body}" ) + return None - if response.status_code != 200: - self.context.logger.error( - f"Could not retrieve data from url {api_url}. Status code {response.status_code}." - ) - return None - - try: - data = json.loads(response.body) - except (ValueError, TypeError) as e: + try: + res = json.loads(response.body) + if res is None: self.context.logger.error( - f"Could not parse response from api, " - f"the following error was encountered {type(e).__name__}: {e}" + f"Could not get pool type for pool ID {pool_id}" ) return None - campaigns = data.get(str(chain_id)) - if not campaigns: - self.context.logger.error( - f"No info available for chainId {chain_id} in response" - ) - continue - - self._filter_campaigns(chain, campaigns, filtered_pools) - - return filtered_pools - - def _filter_campaigns(self, chain, campaigns, filtered_pools): - """Filter campaigns based on allowed assets and LP pools""" - allowed_dexs = self.params.allowed_dexs - - for campaign_list in campaigns.values(): - for campaign in campaign_list.values(): - dex_type = ( - campaign.get("type") - if campaign.get("type") - else campaign.get("ammName") - ) - if not dex_type: - continue - - campaign_apr = campaign.get("apr") - if not campaign_apr: - continue - - campaign_type = campaign.get("campaignType") - if not campaign_type: - continue - - # The pool apr should be greater than the current pool apr - if dex_type in allowed_dexs: - # type 1 and 2 stand for ERC20 and Concentrated liquidity campaigns respectively - # https://docs.merkl.xyz/integrate-merkl/integrate-merkl-to-your-app#merkl-api - if campaign_type in [1, 2]: - if not campaign_apr > self.current_pool.get("apr", 0.0): - self.context.logger.info( - "APR does not exceed the current pool APR" - ) - continue - campaign_pool_address = campaign.get("mainParameter") - if not campaign_pool_address: - continue - current_pool_address = self.current_pool.get("address") - # The pool should not be the current pool - if campaign_pool_address != current_pool_address: - filtered_pools[dex_type][chain].append(campaign) + pools = res.get("data", {}).get("pools", []) + if pools: + return pools[0].get(detail) + return None + except json.JSONDecodeError as e: + self.context.logger.error(f"Error decoding JSON response: {str(e)}") + return None def get_order_of_transactions( self, @@ -1259,6 +1373,7 @@ def _build_exit_pool_action( "chain": self.current_pool.get("chain"), "assets": [tokens[0].get("token"), tokens[1].get("token")], "pool_address": self.current_pool.get("address"), + "pool_type": self.current_pool.get("pool_type"), } if exit_pool_action["dex_type"] == DexTypes.UNISWAP_V3.value: @@ -1404,6 +1519,7 @@ def _build_enter_pool_action(self) -> Dict[str, Any]: ], "pool_address": self.highest_apr_pool.get("pool_address"), "apr": self.highest_apr_pool.get("apr"), + "pool_type": self.highest_apr_pool.get("pool_type"), } def _build_claim_reward_action( @@ -1562,6 +1678,8 @@ def get_next_event(self) -> Generator[None, None, Tuple[str, Dict, Optional[Dict == Action.BRIDGE_SWAP ): self.context.logger.info("Checking the status of swap tx") + # we wait for some time before checking the status of the tx because the tx may take time to reflect on the lifi endpoint + yield from self.sleep(self.params.waiting_period_for_status_check) decision = yield from self.get_decision_on_swap() self.context.logger.info(f"Action to take {decision}") @@ -1570,7 +1688,7 @@ def get_next_event(self) -> Generator[None, None, Tuple[str, Dict, Optional[Dict self.context.logger.info("Waiting for tx to get executed") while decision == Decision.WAIT: # Wait for given time between each status check - yield from self.sleep(self.params.waiting_period_for_retry) + yield from self.sleep(self.params.waiting_period_for_status_check) self.context.logger.info("Checking the status of swap tx again") decision = ( yield from self.get_decision_on_swap() @@ -1608,6 +1726,7 @@ def get_next_event(self) -> Generator[None, None, Tuple[str, Dict, Optional[Dict "dex_type": action["dex_type"], "assets": action["assets"], "apr": action["apr"], + "pool_type": action["pool_type"], } if action.get("dex_type") == DexTypes.UNISWAP_V3.value: token_id, liquidity = yield from self._get_data_from_mint_tx_receipt( @@ -1816,6 +1935,7 @@ def get_enter_pool_tx_hash( pool_address = action.get("pool_address") pool_fee = action.get("pool_fee") safe_address = self.params.safe_contract_addresses.get(action.get("chain")) + pool_type = action.get("pool_type") pool = self.pools.get(dex_type) if not pool: @@ -1839,6 +1959,7 @@ def get_enter_pool_tx_hash( chain=chain, max_amounts_in=max_amounts_in, pool_fee=pool_fee, + pool_type=pool_type, ) if not tx_hash or not contract_address: return None, None, None @@ -1973,6 +2094,7 @@ def get_exit_pool_tx_hash( pool_address = action.get("pool_address") token_id = action.get("token_id") liquidity = action.get("liquidity") + pool_type = action.get("pool_type") safe_address = self.params.safe_contract_addresses.get(action.get("chain")) pool = self.pools.get(dex_type) @@ -1989,6 +2111,7 @@ def get_exit_pool_tx_hash( "assets": assets, "pool_address": pool_address, "chain": chain, + "pool_type": pool_type, } ) @@ -2078,6 +2201,7 @@ def get_transaction_data_for_route( "toChainId": to_chain_id, "toTokenAddress": to_token_address, "options": { + "integrator": INTEGRATOR, "slippage": slippage, "allowSwitchChain": allow_switch_chain, "integrator": "valory", @@ -2446,9 +2570,9 @@ def _simulate_execution_bundle( simulation_results = data.get("simulation_results", []) status = False if simulation_results: - simulation_results = simulation_results[0] - for simulation in simulation_results.values(): - if isinstance(simulation, Dict): + for simulation_result in simulation_results: + simulation = simulation_result.get("simulation", {}) + if simulation.get("status", False): status = simulation.get("status", False) return status diff --git a/packages/valory/skills/liquidity_trader_abci/models.py b/packages/valory/skills/liquidity_trader_abci/models.py index 5dbfa791..3a537934 100644 --- a/packages/valory/skills/liquidity_trader_abci/models.py +++ b/packages/valory/skills/liquidity_trader_abci/models.py @@ -22,7 +22,7 @@ import json import os from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from aea.skills.base import SkillContext @@ -42,10 +42,6 @@ class SharedState(BaseSharedState): abci_app_cls = LiquidityTraderAbciApp - def __init__(self, *args: Any, skill_context: SkillContext, **kwargs: Any) -> None: - """Initialize the state.""" - super().__init__(*args, skill_context=skill_context, **kwargs) - Requests = BaseRequests BenchmarkTool = BaseBenchmarkTool @@ -92,8 +88,8 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._ensure("chain_to_chain_key_mapping", kwargs, str) ) self.max_num_of_retries = self._ensure("max_num_of_retries", kwargs, int) - self.waiting_period_for_retry = self._ensure( - "waiting_period_for_retry", kwargs, int + self.waiting_period_for_status_check = self._ensure( + "waiting_period_for_status_check", kwargs, int ) self.reward_claiming_time_period = self._ensure( "reward_claiming_time_period", kwargs, int @@ -131,6 +127,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: "assets_info_filename", kwargs, str ) self.pool_info_filename: str = self._ensure("pool_info_filename", kwargs, str) + self.balancer_graphql_endpoints = json.loads( + self._ensure("balancer_graphql_endpoints", kwargs, str) + ) super().__init__(*args, **kwargs) diff --git a/packages/valory/skills/liquidity_trader_abci/pools/balancer.py b/packages/valory/skills/liquidity_trader_abci/pools/balancer.py index de541985..73926e25 100644 --- a/packages/valory/skills/liquidity_trader_abci/pools/balancer.py +++ b/packages/valory/skills/liquidity_trader_abci/pools/balancer.py @@ -20,6 +20,7 @@ """This package contains the implemenatation of the BalancerPoolBehaviour class.""" from abc import ABC +from enum import Enum from typing import Any, Dict, Generator, List, Optional, Tuple from packages.valory.contracts.balancer_vault.contract import VaultContract @@ -33,16 +34,66 @@ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +class PoolType(Enum): + """PoolType""" + + WEIGHTED = "Weighted" + COMPOSABLE_STABLE = "ComposableStable" + LIQUIDITY_BOOTSTRAPING = "LiquidityBootstrapping" + META_STABLE = "MetaStable" + STABLE = "Stable" + INVESTMENT = "Investment" + + +class JoinKind: + """JoinKind Enums for different pool types.""" + + # https://docs.balancer.fi/reference/joins-and-exits/pool-joins.html#userdata + class WeightedPool(Enum): + INIT = 0 + EXACT_TOKENS_IN_FOR_BPT_OUT = 1 + TOKEN_IN_FOR_EXACT_BPT_OUT = 2 + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT = 3 + + class StableAndMetaStablePool(Enum): + INIT = 0 + EXACT_TOKENS_IN_FOR_BPT_OUT = 1 + TOKEN_IN_FOR_EXACT_BPT_OUT = 2 + + class ComposableStablePool(Enum): + INIT = 0 + EXACT_TOKENS_IN_FOR_BPT_OUT = 1 + TOKEN_IN_FOR_EXACT_BPT_OUT = 2 + ALL_TOKENS_IN_FOR_EXACT_BPT_OUT = 3 + + +class ExitKind: + """ExitKind Enums for different pool types.""" + + # https://docs.balancer.fi/reference/joins-and-exits/pool-exits.html#userdata + class WeightedPool(Enum): + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0 + EXACT_BPT_IN_FOR_TOKENS_OUT = 1 + BPT_IN_FOR_EXACT_TOKENS_OUT = 2 + MANAGEMENT_FEE_TOKENS_OUT = 3 # for InvestmentPool only + + class StableAndMetaStablePool(Enum): + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0 + EXACT_BPT_IN_FOR_TOKENS_OUT = 1 + BPT_IN_FOR_EXACT_TOKENS_OUT = 2 + + class ComposableStablePool(Enum): + EXACT_BPT_IN_FOR_ONE_TOKEN_OUT = 0 + BPT_IN_FOR_EXACT_TOKENS_OUT = 1 + EXACT_BPT_IN_FOR_ALL_TOKENS_OUT = 2 + + class BalancerPoolBehaviour(PoolBehaviour, ABC): """BalancerPoolBehaviour""" def __init__(self, **kwargs: Any) -> None: """Initialize the balancer pool behaviour.""" super().__init__(**kwargs) - # https://docs.balancer.fi/reference/joins-and-exits/pool-exits.html#userdata - self.exit_kind: int = 1 # EXACT_BPT_IN_FOR_TOKENS_OUT - # https://docs.balancer.fi/reference/joins-and-exits/pool-joins.html#userdata - self.join_kind: int = 1 # EXACT_TOKENS_IN_FOR_BPT_OUT def enter(self, **kwargs: Any) -> Generator[None, None, Optional[Tuple[str, str]]]: """Enter a Balancer pool.""" @@ -51,13 +102,21 @@ def enter(self, **kwargs: Any) -> Generator[None, None, Optional[Tuple[str, str] assets = kwargs.get("assets") chain = kwargs.get("chain") max_amounts_in = kwargs.get("max_amounts_in") - - if not all([pool_address, safe_address, assets, chain, max_amounts_in]): + pool_type = kwargs.get("pool_type") + if not all( + [pool_address, safe_address, assets, chain, max_amounts_in, pool_type] + ): self.context.logger.error( f"Missing required parameters for entering the pool. Here are the kwargs: {kwargs}" ) return None, None + join_kind = self._determine_join_kind(pool_type) + if not join_kind: + self.context.logger.error( + f"Could not determine join kind for pool type {pool_type}" + ) + return None, None # Get vault contract address from balancer weighted pool contract vault_address = self.params.balancer_vault_contract_addresses.get(chain) if not vault_address: @@ -86,7 +145,7 @@ def enter(self, **kwargs: Any) -> Generator[None, None, Optional[Tuple[str, str] recipient=safe_address, assets=assets, max_amounts_in=max_amounts_in, - join_kind=self.join_kind, + join_kind=join_kind, minimum_bpt=minimum_bpt, from_internal_balance=from_internal_balance, chain_id=chain, @@ -102,13 +161,20 @@ def exit( safe_address = kwargs.get("safe_address") assets = kwargs.get("assets") chain = kwargs.get("chain") - - if not all([pool_address, safe_address, assets, chain]): + pool_type = kwargs.get("pool_type") + if not all([pool_address, safe_address, assets, chain, pool_type]): self.context.logger.error( f"Missing required parameters for exiting the pool. Here are the kwargs: {kwargs}" ) return None, None, None + exit_kind = self._determine_exit_kind(pool_type) + if not exit_kind: + self.context.logger.error( + f"Could not determine exit kind for pool type {pool_type}" + ) + return None, None, None + # Get vault contract address from balancer weighted pool contract vault_address = self.params.balancer_vault_contract_addresses.get(chain) if not vault_address: @@ -153,7 +219,7 @@ def exit( recipient=safe_address, assets=assets, min_amounts_out=min_amounts_out, - exit_kind=self.exit_kind, + exit_kind=exit_kind, bpt_amount_in=bpt_amount_in, to_internal_balance=to_internal_balance, chain_id=chain, @@ -222,3 +288,35 @@ def _get_pool_id( self.context.logger.info(f"PoolId for balancer pool {pool_address}: {pool_id}") return pool_id + + def _determine_join_kind(self, pool_type: PoolType) -> Optional[int]: + """Determine the join kind based on the pool type.""" + if pool_type in [ + PoolType.WEIGHTED.value, + PoolType.LIQUIDITY_BOOTSTRAPING.value, + PoolType.INVESTMENT.value, + ]: + return JoinKind.WeightedPool.EXACT_TOKENS_IN_FOR_BPT_OUT.value + elif pool_type in [PoolType.STABLE.value, PoolType.META_STABLE.value]: + return JoinKind.StableAndMetaStablePool.EXACT_TOKENS_IN_FOR_BPT_OUT.value + elif pool_type == PoolType.COMPOSABLE_STABLE.value: + return JoinKind.ComposableStablePool.EXACT_TOKENS_IN_FOR_BPT_OUT.value + else: + self.context.logger.error(f"Unknown pool type: {pool_type}") + return None + + def _determine_exit_kind(self, pool_type: PoolType) -> Optional[int]: + """Determine the exit kind based on the pool type.""" + if pool_type in [ + PoolType.WEIGHTED.value, + PoolType.LIQUIDITY_BOOTSTRAPING.value, + PoolType.INVESTMENT.value, + ]: + return ExitKind.WeightedPool.EXACT_BPT_IN_FOR_TOKENS_OUT.value + elif pool_type in [PoolType.STABLE.value, PoolType.META_STABLE.value]: + return ExitKind.StableAndMetaStablePool.EXACT_BPT_IN_FOR_TOKENS_OUT.value + elif pool_type == PoolType.COMPOSABLE_STABLE.value: + return ExitKind.ComposableStablePool.EXACT_BPT_IN_FOR_ALL_TOKENS_OUT.value + else: + self.context.logger.error(f"Unknown pool type: {pool_type}") + return None diff --git a/packages/valory/skills/liquidity_trader_abci/rounds.py b/packages/valory/skills/liquidity_trader_abci/rounds.py index 4653ab5a..5ae2141c 100644 --- a/packages/valory/skills/liquidity_trader_abci/rounds.py +++ b/packages/valory/skills/liquidity_trader_abci/rounds.py @@ -173,7 +173,12 @@ def is_staking_kpi_met(self) -> Optional[bool]: def chain_id(self) -> Optional[str]: """Get the chain id.""" return cast(str, self.db.get("chain_id", None)) - + + @property + def period_number_at_last_cp(self) -> Optional[int]: + """Get the period number at last cp.""" + return cast(int, self.db.get("period_number_at_last_cp", 0)) + class CallCheckpointRound(CollectSameUntilThresholdRound): """A round for the checkpoint call preparation.""" @@ -216,16 +221,10 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: if synced_data.service_staking_state == StakingState.EVICTED.value: return synced_data, Event.SERVICE_EVICTED - if ( - synced_data.most_voted_tx_hash is None - and synced_data.is_staking_kpi_met == False - ): + if synced_data.is_staking_kpi_met is False: return synced_data, Event.STAKING_KPI_NOT_MET - if ( - synced_data.most_voted_tx_hash is None - and synced_data.is_staking_kpi_met == True - ): + if synced_data.is_staking_kpi_met is True: return synced_data, Event.STAKING_KPI_MET return res @@ -258,14 +257,16 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: if event != Event.DONE: return res - if synced_data.is_staking_kpi_met == True: + if synced_data.most_voted_tx_hash is not None: + return synced_data, Event.SETTLE + + if synced_data.is_staking_kpi_met is True: return synced_data, Event.STAKING_KPI_MET - else: - if synced_data.most_voted_tx_hash is not None: - return synced_data, Event.SETTLE - else: - return synced_data, Event.WAIT_FOR_PERIODS_TO_PASS + if synced_data.is_staking_kpi_met is False: + return synced_data, Event.WAIT_FOR_PERIODS_TO_PASS + + return res class GetPositionsRound(CollectSameUntilThresholdRound): @@ -380,6 +381,12 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: synced_data = SynchronizedData(self.synchronized_data.db) event = submitter_to_event.get(synced_data.tx_submitter, Event.UNRECOGNIZED) + if event == Event.CHECKPOINT_TX_EXECUTED: + synced_data = synced_data.update( + synchronized_data_class=SynchronizedData, + period_number_at_last_cp=synced_data.period_count, + ) + return synced_data, event @@ -485,6 +492,7 @@ class LiquidityTraderAbciApp(AbciApp[Event]): get_name(SynchronizedData.last_reward_claimed_timestamp), get_name(SynchronizedData.min_num_of_safe_tx_required), get_name(SynchronizedData.is_staking_kpi_met), + get_name(SynchronizedData.period_number_at_last_cp), } ) db_pre_conditions: Dict[AppState, Set[str]] = { diff --git a/packages/valory/skills/liquidity_trader_abci/skill.yaml b/packages/valory/skills/liquidity_trader_abci/skill.yaml index ee2d93f1..434e55f2 100644 --- a/packages/valory/skills/liquidity_trader_abci/skill.yaml +++ b/packages/valory/skills/liquidity_trader_abci/skill.yaml @@ -7,16 +7,16 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeia7bn2ahqqwkf63ptje6rfnftuwrsp33sswgpcbh5osbesxxr6g4m - behaviours.py: bafybeicm2544nuorncipsbt4estpihctu66jh5qbzsnmsiggxstx7obe2e + behaviours.py: bafybeiagypdfelpxoljhs2vpirghttjald27izk53m72vmmqmg7qbzmphq dialogues.py: bafybeiay23otskx2go5xhtgdwfw2kd6rxd62sxxdu3njv7hageorl5zxzm fsm_specification.yaml: bafybeif4v5cy62pcxtilwbxnxr3hbiwmwlmw2xucc6wl3paa6rsqxlndmm handlers.py: bafybeidxw2lvgiifmo4siobpwuwbxscuifrdo3gnkjyn6bgexotj5f7zf4 - models.py: bafybeicu6nuwivewuhjvoq6p362rz5bvwtchqss5ce6b32km2brvybuzdi + models.py: bafybeibmotfx4s5zq352dxf4llqqkwa4ukcdantmzba3yolsj6frwx7t2i payloads.py: bafybeigi7t4fe3gr4reuq2d2xz4qblbxlo6wwy2hwh7htaaihjoya7uqky pool_behaviour.py: bafybeiaheuesscgqzwjbpyrezgwpdbdfurlmfwbc462qv6rblwwxlx5dpm - pools/balancer.py: bafybeieo53qtt557632vtp3x3huagdpqevptextbwd7euk6hpoz6ybtuci + pools/balancer.py: bafybeigoiczoramz4uvhvnonprvik4jxkhnnoi3dtmy36gebfu2wzd3ndq pools/uniswap.py: bafybeif2cjbtjh5altlgranmgrif4yaevnn344fn3askbjde5h4y4rh2mq - rounds.py: bafybeid7jpwkznn5nmsqnspc6ykhfddwujmdxwkqmwjchmw6k7jjqgwg6e + rounds.py: bafybeicj7pyt6norwok2p55fqrzdtoylxwpix2ludkswgymxumld6mpcma strategies/simple_strategy.py: bafybeig2iygxz5gewmiyeawubs5f4pr5e3st22lmdkxthlfq7t5kbluw4a strategy_behaviour.py: bafybeidk6sorg47kuuubamcccksi65x3txldyo7y2hm5opbye2ghmz2ljy fingerprint_ignore_patterns: [] @@ -164,7 +164,7 @@ models: balancer_vault_contract_addresses: '{"optimism":"0xBA12222222228d8Ba445958a75a0704d566BF2C8","base":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"}' uniswap_position_manager_contract_addresses: '{"optimism":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88","base":"0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1"}' chain_to_chain_key_mapping: '{"optimism":"opt","base":"bas"}' - waiting_period_for_retry: 5 + waiting_period_for_status_check: 5 max_num_of_retries: 5 reward_claiming_time_period: 28800 merkl_distributor_contract_addresses: '{"optimism":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae","base":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae"}' @@ -182,7 +182,8 @@ models: store_path: data assets_info_filename: assets.json pool_info_filename: current_pool.json - merkl_fetch_campaigns_args: '{"url":"https://api.merkl.xyz/v3/campaigns","creator":"superfest","live":"true"}' + merkl_fetch_campaigns_args: '{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}' + balancer_graphql_endpoints: '{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}' class_name: Params requests: args: {} diff --git a/packages/valory/skills/optimus_abci/skill.yaml b/packages/valory/skills/optimus_abci/skill.yaml index 55309186..37abfdbe 100644 --- a/packages/valory/skills/optimus_abci/skill.yaml +++ b/packages/valory/skills/optimus_abci/skill.yaml @@ -22,7 +22,7 @@ skills: - valory/registration_abci:0.1.0:bafybeicnth5q4httefsusywx3zrrq4al47owvge72dqf2fziruicq6hqta - valory/reset_pause_abci:0.1.0:bafybeievjciqdvxhqxfjd4whqs27h6qbxqzrae7wwj7fpvxlvmtw3x35im - valory/termination_abci:0.1.0:bafybeid54buqxipiuduw7b6nnliiwsxajnltseuroad53wukfonpxca2om -- valory/liquidity_trader_abci:0.1.0:bafybeihii4e64akqkelii37havxmet3qeg5vlv4vg2toekoaww6tk7zudi +- valory/liquidity_trader_abci:0.1.0:bafybeiftxq6ipny6wyir3vobayjav7fok5ys5cgwoaewlf6gwoleior364 - valory/transaction_settlement_abci:0.1.0:bafybeihq2yenstblmaadzcjousowj5kfn5l7ns5pxweq2gcrsczfyq5wzm behaviours: main: @@ -158,7 +158,7 @@ models: balancer_vault_contract_addresses: '{"optimism":"0xBA12222222228d8Ba445958a75a0704d566BF2C8","base":"0xBA12222222228d8Ba445958a75a0704d566BF2C8"}' uniswap_position_manager_contract_addresses: '{"optimism":"0xC36442b4a4522E871399CD717aBDD847Ab11FE88","base":"0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1"}' chain_to_chain_key_mapping: '{"optimism":"opt","base":"bas"}' - waiting_period_for_retry: 5 + waiting_period_for_status_check: 5 max_num_of_retries: 5 reward_claiming_time_period: 28800 merkl_distributor_contract_addresses: '{"optimism":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae","base":"0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae"}' @@ -176,7 +176,8 @@ models: store_path: data assets_info_filename: assets.json pool_info_filename: current_pool.json - merkl_fetch_campaigns_args: '{"url":"https://api.merkl.xyz/v3/campaigns","creator":"superfest","live":"true"}' + merkl_fetch_campaigns_args: '{"url":"https://api.merkl.xyz/v3/campaigns","creator":"","live":"true"}' + balancer_graphql_endpoints: '{"optimism":"https://api.studio.thegraph.com/query/75376/balancer-optimism-v2/version/latest","base":"https://api.studio.thegraph.com/query/24660/balancer-base-v2/version/latest"}' class_name: Params randomness_api: args: