diff --git a/report.py b/report.py index 0312038..ace4112 100644 --- a/report.py +++ b/report.py @@ -25,9 +25,12 @@ import time import traceback from argparse import ArgumentParser +from dotenv import dotenv_values from enum import Enum from pathlib import Path -from typing import Any +import requests +import sys +from typing import Any, List import docker import trades @@ -46,21 +49,31 @@ SCRIPT_PATH = Path(__file__).resolve().parent STORE_PATH = Path(SCRIPT_PATH, ".trader_runner") +DOTENV_PATH = Path(STORE_PATH, ".env") RPC_PATH = Path(STORE_PATH, "rpc.txt") AGENT_KEYS_JSON_PATH = Path(STORE_PATH, "keys.json") OPERATOR_KEYS_JSON_PATH = Path(STORE_PATH, "operator_keys.json") SAFE_ADDRESS_PATH = Path(STORE_PATH, "service_safe_address.txt") SERVICE_ID_PATH = Path(STORE_PATH, "service_id.txt") -SERVICE_STAKING_CONTRACT_ADDRESS = "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237" -SERVICE_STAKING_TOKEN_JSON_PATH = Path( +STAKING_TOKEN_JSON_PATH = Path( SCRIPT_PATH, "trader", "packages", "valory", "contracts", - "service_staking_token", + "staking_token", "build", - "ServiceStakingToken.json", + "StakingToken.json", +) +ACTIVITY_CHECKER_JSON_PATH = Path( + SCRIPT_PATH, + "trader", + "packages", + "valory", + "contracts", + "mech_activity", + "build", + "MechActivity.json", ) SERVICE_REGISTRY_L2_JSON_PATH = Path( SCRIPT_PATH, @@ -94,7 +107,6 @@ OPERATOR_XDAI_BALANCE_THRESHOLD = 50000000000000000 MECH_REQUESTS_PER_EPOCH_THRESHOLD = 10 TRADES_LOOKBACK_DAYS = 3 -AGENT_ID = 14 OUTPUT_WIDTH = 80 @@ -228,6 +240,8 @@ def _parse_args() -> Any: with open(RPC_PATH, "r", encoding="utf-8") as file: rpc = file.read().strip() + env_file_vars = dotenv_values(DOTENV_PATH) + # Prediction market trading mech_requests = trades.get_mech_requests(safe_address) mech_statistics = trades.get_mech_statistics(mech_requests) @@ -247,38 +261,52 @@ def _parse_args() -> Any: try: w3 = Web3(HTTPProvider(rpc)) - with open(SERVICE_STAKING_TOKEN_JSON_PATH, "r", encoding="utf-8") as file: - service_staking_token_data = json.load(file) - service_staking_token_abi = service_staking_token_data.get("abi", []) - service_staking_token_contract = w3.eth.contract( - address=SERVICE_STAKING_CONTRACT_ADDRESS, abi=service_staking_token_abi + staking_token_address = env_file_vars.get("CUSTOM_STAKING_ADDRESS") + with open(STAKING_TOKEN_JSON_PATH, "r", encoding="utf-8") as file: + staking_token_data = json.load(file) + + staking_token_abi = staking_token_data.get("abi", []) + staking_token_contract = w3.eth.contract( + address=staking_token_address, abi=staking_token_abi # type: ignore ) - service_staking_state = StakingState( - service_staking_token_contract.functions.getServiceStakingState( + + staking_state = StakingState( + staking_token_contract.functions.getStakingState( service_id ).call() ) is_staked = ( - service_staking_state == StakingState.STAKED - or service_staking_state == StakingState.EVICTED + staking_state == StakingState.STAKED + or staking_state == StakingState.EVICTED ) _print_status("Is service staked?", _color_bool(is_staked, "Yes", "No")) - if service_staking_state == StakingState.STAKED: - _print_status("Staking state", service_staking_state.name) - elif service_staking_state == StakingState.EVICTED: - _print_status("Staking state", _color_string(service_staking_state.name, ColorCode.RED)) - + if is_staked: + _print_status("Staking program", env_file_vars.get("STAKING_PROGRAM")) # type: ignore + if staking_state == StakingState.STAKED: + _print_status("Staking state", staking_state.name) + elif staking_state == StakingState.EVICTED: + _print_status("Staking state", _color_string(staking_state.name, ColorCode.RED)) if is_staked: + + activity_checker_address = staking_token_contract.functions.activityChecker().call() + with open(ACTIVITY_CHECKER_JSON_PATH, "r", encoding="utf-8") as file: + activity_checker_data = json.load(file) + + activity_checker_abi = activity_checker_data.get("abi", []) + activity_checker_contract = w3.eth.contract( + address=activity_checker_address, abi=activity_checker_abi # type: ignore + ) + with open( SERVICE_REGISTRY_TOKEN_UTILITY_JSON_PATH, "r", encoding="utf-8" ) as file: service_registry_token_utility_data = json.load(file) service_registry_token_utility_contract_address = ( - service_staking_token_contract.functions.serviceRegistryTokenUtility().call() + staking_token_contract.functions.serviceRegistryTokenUtility().call() ) service_registry_token_utility_abi = ( service_registry_token_utility_data.get("abi", []) @@ -288,13 +316,14 @@ def _parse_args() -> Any: abi=service_registry_token_utility_abi, ) + mech_contract_address = env_file_vars.get("MECH_CONTRACT_ADDRESS") with open(MECH_CONTRACT_JSON_PATH, "r", encoding="utf-8") as file: mech_contract_data = json.load(file) mech_contract_abi = mech_contract_data.get("abi", []) mech_contract = w3.eth.contract( - address=MECH_CONTRACT_ADDRESS, abi=mech_contract_abi + address=mech_contract_address, abi=mech_contract_abi # type: ignore ) security_deposit = ( @@ -302,11 +331,12 @@ def _parse_args() -> Any: operator_address, service_id ).call() ) + agent_id = int(env_file_vars.get("AGENT_ID").strip()) agent_bond = service_registry_token_utility_contract.functions.getAgentBond( - service_id, AGENT_ID + service_id, agent_id ).call() min_staking_deposit = ( - service_staking_token_contract.functions.minStakingDeposit().call() + staking_token_contract.functions.minStakingDeposit().call() ) # In the setting 1 agent instance as of now: minOwnerBond = minStakingDeposit @@ -320,30 +350,30 @@ def _parse_args() -> Any: f"{wei_to_olas(agent_bond)} {_warning_message(agent_bond, min_staking_deposit)}", ) - service_info = service_staking_token_contract.functions.mapServiceInfo( + service_info = staking_token_contract.functions.mapServiceInfo( service_id ).call() rewards = service_info[3] _print_status("Accrued rewards", f"{wei_to_olas(rewards)}") liveness_ratio = ( - service_staking_token_contract.functions.livenessRatio().call() + activity_checker_contract.functions.livenessRatio().call() ) mech_requests_24h_threshold = math.ceil( (liveness_ratio * 60 * 60 * 24) / 10**18 ) next_checkpoint_ts = ( - service_staking_token_contract.functions.getNextRewardCheckpointTimestamp().call() + staking_token_contract.functions.getNextRewardCheckpointTimestamp().call() ) liveness_period = ( - service_staking_token_contract.functions.livenessPeriod().call() + staking_token_contract.functions.livenessPeriod().call() ) last_checkpoint_ts = next_checkpoint_ts - liveness_period mech_request_count = mech_contract.functions.getRequestsCount(safe_address).call() mech_request_count_on_last_checkpoint = ( - service_staking_token_contract.functions.getServiceInfo(service_id).call() + staking_token_contract.functions.getServiceInfo(service_id).call() )[2][1] mech_requests_since_last_cp = mech_request_count - mech_request_count_on_last_checkpoint # mech_requests_current_epoch = _get_mech_requests_count( diff --git a/run_service.sh b/run_service.sh index c8133f0..0f385f6 100755 --- a/run_service.sh +++ b/run_service.sh @@ -273,6 +273,13 @@ get_on_chain_service_state() { echo "$state" } +get_on_chain_agent_ids() { + local service_id="$1" + local service_info=$(poetry run autonomy service --use-custom-chain info "$service_id") + local agent_ids="$(echo "$service_info" | awk '/Cannonical Agents/ {sub(/\|[ \t]*Cannonical Agents[ \t]*\|[ \t]*/, ""); sub(/[ \t]*\|[ \t]*/, ""); print}')" + echo "$agent_ids" +} + # Move a file if it exists move_if_exists() { local source_file="$1" @@ -430,30 +437,6 @@ perform_staking_ops() { echo "" } -# Prompt user for staking preference -prompt_use_staking() { - while true; do - echo "Use staking?" - echo "------------" - read -p "Do you want to stake this service? (yes/no): " use_staking - - case "$use_staking" in - [Yy]|[Yy][Ee][Ss]) - USE_STAKING="true" - break - ;; - [Nn]|[Nn][Oo]) - USE_STAKING="false" - break - ;; - *) - echo "Please enter 'yes' or 'no'." - ;; - esac - done - echo "" -} - # Prompt user for subgraph API key prompt_subgraph_api_key() { echo "Provide a Subgraph API key" @@ -517,6 +500,15 @@ dotenv_set_key() { export "$key_to_set=$value_to_set" } +export_dotenv() { + local dotenv_path="$1" + unamestr=$(uname) + if [ "$unamestr" = 'Linux' ]; then + export $(grep -v '^#' $dotenv_path | xargs -d '\n') + elif [ "$unamestr" = 'FreeBSD' ] || [ "$unamestr" = 'Darwin' ]; then + export $(grep -v '^#' $dotenv_path | xargs -0) + fi +} store=".trader_runner" path_to_store="$PWD/$store/" @@ -544,8 +536,6 @@ create_storage() { ask_confirm_password - # Prompt use staking - prompt_use_staking prompt_subgraph_api_key verify_staking_slots @@ -557,10 +547,6 @@ create_storage() { ' Please back up this folder and be cautious if you are modifying or sharing these files to avoid potential asset loss.' > "../$store_readme_path" dotenv_set_key "../$env_file_path" "SUBGRAPH_API_KEY" "$SUBGRAPH_API_KEY" true - dotenv_set_key "../$env_file_path" "USE_STAKING" "$USE_STAKING" - - AGENT_ID=14 - dotenv_set_key "../$env_file_path" "AGENT_ID" "$AGENT_ID" # Generate the RPC file echo -n "$rpc" > "../$rpc_path" @@ -621,8 +607,6 @@ try_read_storage() { fi done - unset USE_STAKING - unset AGENT_ID source "$env_file_path" rpc=$(cat $rpc_path) @@ -638,18 +622,6 @@ try_read_storage() { dotenv_set_key "$env_file_path" "SUBGRAPH_API_KEY" "$SUBGRAPH_API_KEY" true fi - # INFO: This is a fix to avoid corrupting already-created stores - if [ -z "$USE_STAKING" ]; then - prompt_use_staking - dotenv_set_key "$env_file_path" "USE_STAKING" "$USE_STAKING" - fi - - # INFO: This is a fix to avoid corrupting already-created stores - if [ -z "$AGENT_ID" ]; then - AGENT_ID=14 - dotenv_set_key "$env_file_path" "AGENT_ID" "$AGENT_ID" - fi - ask_password_if_needed else first_run=true @@ -675,33 +647,19 @@ service_version="v0.18.0" # Define constants for on-chain interaction gnosis_chain_id=100 n_agents=1 -olas_balance_required_to_bond=10000000000000000000 -olas_balance_required_to_stake=10000000000000000000 -xdai_balance_required_to_bond=10000000000000000 +MIN_STAKING_BOND_XDAI=10000000000000000 suggested_top_up_default=50000000000000000 suggested_safe_top_up_default=500000000000000000 export RPC_RETRIES=40 export RPC_TIMEOUT_SECONDS=120 + export CUSTOM_SERVICE_MANAGER_ADDRESS="0x04b0007b2aFb398015B76e5f22993a1fddF83644" -export CUSTOM_SERVICE_REGISTRY_ADDRESS="0x9338b5153AE39BB89f50468E608eD9d764B755fD" -export CUSTOM_STAKING_ADDRESS="0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237" -export CUSTOM_OLAS_ADDRESS="0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" -export CUSTOM_SERVICE_REGISTRY_TOKEN_UTILITY_ADDRESS="0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8" export CUSTOM_GNOSIS_SAFE_PROXY_FACTORY_ADDRESS="0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE" export CUSTOM_GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_ADDRESS="0x6e7f594f680f7aBad18b7a63de50F0FeE47dfD06" export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" export WXDAI_ADDRESS="0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" -export MECH_CONTRACT_ADDRESS="0x77af31De935740567Cf4fF1986D04B2c964A786a" - -# check if USE_NEVERMINED is set to true -if [ "$USE_NEVERMINED" == "true" ]; -then - echo "A Nevermined subscription will be used to pay for the mech requests." - export MECH_CONTRACT_ADDRESS="0x327E26bDF1CfEa50BFAe35643B23D5268E41F7F9" - export AGENT_REGISTRY_ADDRESS="0xAed729d4f4b895d8ca84ba022675bB0C44d2cD52" - export MECH_REQUEST_PRICE=0 -fi +export OPEN_AUTONOMY_SUBGRAPH_URL="https://subgraph.autonolas.tech/subgraphs/name/autonolas-staging" sleep_duration=12 @@ -864,12 +822,11 @@ echo "" echo "-----------------------------------------" echo "Checking Autonolas Protocol service state" echo "-----------------------------------------" +echo "" -# We set by default AGENT_ID=14. In Everest the AGENT_ID was 12. -# This script does not allow to stake on Everest anymore, therefore -# all stores must be correctly updated with AGENT_ID=14. -AGENT_ID=14 -dotenv_set_key "../$env_file_path" "AGENT_ID" "$AGENT_ID" +# Prompt use staking +poetry run python "../scripts/choose_staking.py" +export_dotenv "../$env_file_path" if [ -z ${service_id+x} ]; then # Check balances @@ -894,10 +851,10 @@ if [ -z ${service_id+x} ]; then --threshold $n_agents" if [ "${USE_STAKING}" = true ]; then - cost_of_bonding=$olas_balance_required_to_bond + cost_of_bonding=$MIN_STAKING_BOND_OLAS cmd+=" -c $cost_of_bonding --token $CUSTOM_OLAS_ADDRESS" else - cost_of_bonding=$xdai_balance_required_to_bond + cost_of_bonding=$MIN_STAKING_BOND_XDAI cmd+=" -c $cost_of_bonding" fi service_id=$(eval $cmd) @@ -919,16 +876,19 @@ packages="packages/packages.json" local_service_hash="$(grep 'service/valory/trader' $packages | awk -F: '{print $2}' | tr -d '", ' | head -n 1)" remote_service_hash=$(poetry run python "../scripts/service_hash.py") operator_address=$(get_address "../$operator_keys_file") +on_chain_agent_id=$(get_on_chain_agent_ids "$service_id") -if [ "$local_service_hash" != "$remote_service_hash" ]; then +if [ "$local_service_hash" != "$remote_service_hash" ] || [ "$on_chain_agent_id" != "$AGENT_ID" ]; then echo "" echo "WARNING: Your on-chain service configuration is out-of-date" echo "-----------------------------------------------------------" - echo "Your currently minted on-chain service (id $service_id) mismatches the local trader service ($service_version):" + echo "Your currently minted on-chain service (id $service_id) mismatches the local configuration:" echo " - Local service hash ($service_version): $local_service_hash" - echo " - On-chain service hash (id $service_id): $remote_service_hash" + echo " - On-chain service hash: $remote_service_hash" + echo " - Local agent id: $AGENT_ID" + echo " - On-chain agent id: $on_chain_agent_id" echo "" - echo "This is most likely caused due to an update of the trader service code." + echo "This is most likely caused due to an update of the trader service code or agent id." echo "The script will proceed now to update the on-chain service." echo "The operator and agent addresses need to have enough funds to complete the process." echo "" @@ -1013,10 +973,10 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" export cmd="" if [ "${USE_STAKING}" = true ]; then - cost_of_bonding=$olas_balance_required_to_bond + cost_of_bonding=$MIN_STAKING_BOND_OLAS poetry run python "../scripts/update_service.py" "../$operator_pkey_path" "$nft" "$AGENT_ID" "$service_id" "$CUSTOM_OLAS_ADDRESS" "$cost_of_bonding" "packages/valory/services/trader/" "$rpc" $password_argument else - cost_of_bonding=$xdai_balance_required_to_bond + cost_of_bonding=$MIN_STAKING_BOND_XDAI cmd="poetry run autonomy mint \ --retries $RPC_RETRIES \ --timeout $RPC_TIMEOUT_SECONDS \ @@ -1057,11 +1017,11 @@ if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then echo "[Service owner] Activating registration for on-chain service $service_id..." export cmd="poetry run autonomy service --retries $RPC_RETRIES --timeout $RPC_TIMEOUT_SECONDS --use-custom-chain activate --key "../$operator_pkey_path" $password_argument "$service_id"" if [ "${USE_STAKING}" = true ]; then - minimum_olas_balance=$($PYTHON_CMD -c "print(int($olas_balance_required_to_bond) + int($olas_balance_required_to_stake))") + minimum_olas_balance=$($PYTHON_CMD -c "print(int($MIN_STAKING_DEPOSIT_OLAS) + int($MIN_STAKING_BOND_OLAS))") echo "Your service is using staking. Therefore, you need to provide a total of $(wei_to_dai "$minimum_olas_balance") OLAS to your owner/operator's address." - echo " $(wei_to_dai "$olas_balance_required_to_bond") OLAS for security deposit (service owner)" + echo " $(wei_to_dai "$MIN_STAKING_DEPOSIT_OLAS") OLAS for security deposit (service owner)" echo " +" - echo " $(wei_to_dai "$olas_balance_required_to_stake") OLAS for slashable bond (operator)." + echo " $(wei_to_dai "$MIN_STAKING_BOND_OLAS") OLAS for slashable bond (operator)." echo "" ensure_erc20_balance "$operator_address" $minimum_olas_balance "owner/operator's address" $CUSTOM_OLAS_ADDRESS "OLAS" @@ -1171,7 +1131,7 @@ export STOP_TRADING_IF_STAKING_KPI_MET=true export RESET_PAUSE_DURATION=45 export MECH_WRAPPED_NATIVE_TOKEN_ADDRESS=$WXDAI_ADDRESS export MECH_CHAIN_ID=ethereum -export TOOLS_ACCURACY_HASH=Qmem7ME7CCQH8bU26NQ6R8rV4Eu329dMuErSvjwoSvqfEh +export TOOLS_ACCURACY_HASH=QmexjVcbhh7sMAKmRtxLbgZiiGKdGpHHMYAWecVq7riAD1 if [ -n "$SUBGRAPH_API_KEY" ]; then export CONDITIONAL_TOKENS_SUBGRAPH_URL="https://gateway-arbitrum.network.thegraph.com/api/$SUBGRAPH_API_KEY/subgraphs/id/7s9rGBffUTL8kDZuxvvpuc46v44iuDarbrADBFw5uVp2" diff --git a/scripts/choose_staking.py b/scripts/choose_staking.py new file mode 100644 index 0000000..468cb1c --- /dev/null +++ b/scripts/choose_staking.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023-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. +# +# ------------------------------------------------------------------------------ + +import argparse +import requests +import sys +import textwrap +import json +from dotenv import dotenv_values, set_key, unset_key +from pathlib import Path +from typing import Any, Dict, List +from web3 import Web3 +from eth_utils import keccak + +SCRIPT_PATH = Path(__file__).resolve().parent +STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") +DOTENV_PATH = Path(STORE_PATH, ".env") +RPC_PATH = Path(STORE_PATH, "rpc.txt") + +IPFS_ADDRESS = "https://gateway.autonolas.tech/ipfs/f01701220{}" +NEVERMINED_MECH_CONTRACT_ADDRESS = "0x327E26bDF1CfEa50BFAe35643B23D5268E41F7F9" +NEVERMINED_AGENT_REGISTRY_ADDRESS = "0xAed729d4f4b895d8ca84ba022675bB0C44d2cD52" +NEVERMINED_MECH_REQUEST_PRICE = "0" +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +DEPRECATED_TEXT = "(DEPRECATED)" + + +def _fetch_json(url): + response = requests.get(url) + response.raise_for_status() + return response.json() + + +# Information stored in the "deployment" key is used only to retrieve "stakingTokenInstanceAddress" (proxy) +# and "stakingTokenAddress" (implementation). The rest of the parameters are read on-chain. +STAKING_PROGRAMS = { + "no_staking": { + "name": "No staking", + "description": "Your Olas Predict agent will still actively participate in prediction markets, but it will not be staked within any staking program.", + "deployment": { + "stakingTokenAddress": "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237", + "stakingTokenInstanceAddress": "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237" + } + }, + "quickstart_beta_hobbyist": { + "name": "Quickstart Beta - Hobbyist", + "description": "The Quickstart Beta - Hobbyist staking contract offers 100 slots for operators running Olas Predict agents with the quickstart. It is designed as a step up from Coastal Staker Expeditions, requiring 100 OLAS for staking. The rewards are also more attractive than with Coastal Staker Expeditions.", + # https://github.com/valory-xyz/autonolas-staking-programmes/blob/main/scripts/deployment/globals_gnosis_mainnet_qs_beta_hobbyist.json + "deployment": { + "stakingTokenAddress": "0xEa00be6690a871827fAfD705440D20dd75e67AB1", + "stakingTokenInstanceAddress": "0x389B46c259631Acd6a69Bde8B6cEe218230bAE8C" + } + }, + "quickstart_beta_expert": { + "name": "Quickstart Beta - Expert", + "description": "The Quickstart Beta - Expert staking contract offers 20 slots for operators running Olas Predict agents with the quickstart. It is designed for professional agent operators, requiring 1000 OLAS for staking. The rewards are proportional to the Quickstart Beta - Hobbyist.", + # https://github.com/valory-xyz/autonolas-staking-programmes/blob/main/scripts/deployment/globals_gnosis_mainnet_qs_beta_expert.json + "deployment": { + "stakingTokenAddress": "0xEa00be6690a871827fAfD705440D20dd75e67AB1", + "stakingTokenInstanceAddress": "0x5344B7DD311e5d3DdDd46A4f71481bD7b05AAA3e" + } + }, +} + +DEPRECATED_STAKING_PROGRAMS = { + "quickstart_alpha_everest": { + "name": "Quickstart Alpha - Everest", + "description": "", + "deployment": { + "stakingTokenAddress": "0x5add592ce0a1B5DceCebB5Dcac086Cd9F9e3eA5C", + "stakingTokenInstanceAddress": "0x5add592ce0a1B5DceCebB5Dcac086Cd9F9e3eA5C" + } + }, + "quickstart_alpha_alpine": { + "name": "Quickstart Alpha - Alpine", + "description": "", + "deployment": { + "stakingTokenAddress": "0x2Ef503950Be67a98746F484DA0bBAdA339DF3326", + "stakingTokenInstanceAddress": "0x2Ef503950Be67a98746F484DA0bBAdA339DF3326" + } + }, + "quickstart_alpha_coastal": { + "name": "Quickstart Alpha - Coastal", + "description": "The Quickstart Alpha - Coastal offers 100 slots for operators running Olas Predict agents with the quickstart. It requires 20 OLAS for staking.", + "deployment": { + "stakingTokenAddress": "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237", + "stakingTokenInstanceAddress": "0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237" + } + } +} + + +def _prompt_select_staking_program() -> str: + env_file_vars = dotenv_values(DOTENV_PATH) + + program_id = None + if 'STAKING_PROGRAM' in env_file_vars: + print("The staking program is already selected.") + + program_id = env_file_vars.get('STAKING_PROGRAM') + if program_id not in STAKING_PROGRAMS: + print(f"WARNING: Selected staking program {program_id} is unknown.") + print("") + program_id = None + + if not program_id: + print("Please, select your staking program preference") + print("----------------------------------------------") + ids = list(STAKING_PROGRAMS.keys()) + for index, key in enumerate(ids): + program = STAKING_PROGRAMS[key] + wrapped_description = textwrap.fill(program['description'], width=80, initial_indent=' ', subsequent_indent=' ') + print(f"{index + 1}) {program['name']}\n{wrapped_description}\n") + + while True: + try: + choice = int(input(f"Enter your choice (1 - {len(ids)}): ")) - 1 + if not (0 <= choice < len(ids)): + raise ValueError + program_id = ids[choice] + break + except ValueError: + print(f"Please enter a valid option (1 - {len(ids)}).") + + print(f"Selected staking program: {STAKING_PROGRAMS[program_id]['name']}") + print("") + return program_id + + +def _get_abi(contract_address: str) -> List: + contract_abi_url = "https://gnosis.blockscout.com/api/v2/smart-contracts/{contract_address}" + response = requests.get(contract_abi_url.format(contract_address=contract_address)).json() + + if "result" in response: + result = response["result"] + try: + abi = json.loads(result) + except json.JSONDecodeError: + print("Error: Failed to parse 'result' field as JSON") + sys.exit(1) + else: + abi = response.get("abi") + + return abi if abi else [] + + +def _get_staking_env_variables(program_id: str) -> Dict[str, str]: + staking_program_data = STAKING_PROGRAMS.get(program_id) + + with open(RPC_PATH, 'r', encoding="utf-8") as file: + rpc = file.read().strip() + + w3 = Web3(Web3.HTTPProvider(rpc)) + staking_token_instance_address = staking_program_data["deployment"]["stakingTokenInstanceAddress"] # Instance/proxy + staking_token_address = staking_program_data["deployment"]["stakingTokenAddress"] # Implementation + abi = _get_abi(staking_token_address) + staking_token_contract = w3.eth.contract(address=staking_token_instance_address, abi=abi) + + agent_id = staking_token_contract.functions.agentIds(0).call() + service_registry = staking_token_contract.functions.serviceRegistry().call() + staking_token = staking_token_contract.functions.stakingToken().call() + service_registry_token_utility = staking_token_contract.functions.serviceRegistryTokenUtility().call() + min_staking_deposit = staking_token_contract.functions.minStakingDeposit().call() + min_staking_bond = min_staking_deposit + + if 'activityChecker' in [func.fn_name for func in staking_token_contract.all_functions()]: + activity_checker = staking_token_contract.functions.activityChecker().call() + abi = _get_abi(activity_checker) + activity_checker_contract = w3.eth.contract(address=activity_checker, abi=abi) + agent_mech = activity_checker_contract.functions.agentMech().call() + else: + activity_checker = ZERO_ADDRESS + agent_mech = staking_token_contract.functions.agentMech().call() + + if program_id == "no_staking": + use_staking = "false" + else: + use_staking = "true" + + return { + "USE_STAKING": use_staking, + "STAKING_PROGRAM": program_id, + "CUSTOM_STAKING_ADDRESS": staking_token_instance_address, + "AGENT_ID": agent_id, + "CUSTOM_SERVICE_REGISTRY_ADDRESS": service_registry, + "CUSTOM_OLAS_ADDRESS": staking_token, + "CUSTOM_SERVICE_REGISTRY_TOKEN_UTILITY_ADDRESS": service_registry_token_utility, + "MECH_ACTIVITY_CHECKER_CONTRACT": activity_checker, + "MECH_CONTRACT_ADDRESS": agent_mech, + "MIN_STAKING_DEPOSIT_OLAS": min_staking_deposit, + "MIN_STAKING_BOND_OLAS": min_staking_bond + } + + +def _set_dotenv_file_variables(env_vars: Dict[str, str]) -> None: + for key, value in env_vars.items(): + if value: + set_key(dotenv_path=DOTENV_PATH, key_to_set=key, value_to_set=value, quote_mode="never") + else: + unset_key(dotenv_path=DOTENV_PATH, key_to_unset=key) + + +def _get_nevermined_env_variables() -> Dict[str, str]: + env_file_vars = dotenv_values(DOTENV_PATH) + use_nevermined = False + + if 'USE_NEVERMINED' not in env_file_vars: + set_key(dotenv_path=DOTENV_PATH, key_to_set="USE_NEVERMINED", value_to_set="false", quote_mode="never") + elif env_file_vars.get('USE_NEVERMINED').strip() not in ("True", "true"): + set_key(dotenv_path=DOTENV_PATH, key_to_set="USE_NEVERMINED", value_to_set="false", quote_mode="never") + else: + use_nevermined = True + + if use_nevermined: + print("A Nevermined subscription will be used to pay for the mech requests.") + return { + "MECH_CONTRACT_ADDRESS": NEVERMINED_MECH_CONTRACT_ADDRESS, + "AGENT_REGISTRY_ADDRESS": NEVERMINED_AGENT_REGISTRY_ADDRESS, + "MECH_REQUEST_PRICE": NEVERMINED_MECH_REQUEST_PRICE + } + else: + print("No Nevermined subscription set.") + return { + "AGENT_REGISTRY_ADDRESS": "", + "MECH_REQUEST_PRICE": "" + } + + +def main() -> None: + parser = argparse.ArgumentParser(description="Set up staking configuration.") + parser.add_argument("--reset", action="store_true", help="Reset USE_STAKING and STAKING_PROGRAM in .env file") + args = parser.parse_args() + + if args.reset: + unset_key(dotenv_path=DOTENV_PATH, key_to_unset="USE_STAKING") + unset_key(dotenv_path=DOTENV_PATH, key_to_unset="STAKING_PROGRAM") + print(f"Environment variables USE_STAKING and STAKING_PROGRAM have been reset in '{DOTENV_PATH}'.") + print("You can now execute './run_service.sh' and select a different staking program.") + print("") + return + + program_id = _prompt_select_staking_program() + + print("Populating staking program variables in the .env file") + print("") + staking_env_variables = _get_staking_env_variables(program_id) + _set_dotenv_file_variables(staking_env_variables) + + print("Populating Nevermined variables in the .env file") + print("") + nevermined_env_variables = _get_nevermined_env_variables() + _set_dotenv_file_variables(nevermined_env_variables) + + +if __name__ == "__main__": + main() diff --git a/scripts/staking.py b/scripts/staking.py index 55f9558..610db5c 100644 --- a/scripts/staking.py +++ b/scripts/staking.py @@ -25,10 +25,12 @@ import time import traceback from datetime import datetime +from dotenv import dotenv_values from pathlib import Path import dotenv from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto +from choose_staking import STAKING_PROGRAMS, DEPRECATED_STAKING_PROGRAMS from utils import ( get_available_rewards, get_available_staking_slots, @@ -45,12 +47,6 @@ ) -OLD_STAKING_PROGRAMS = { - "Everest": "0x5add592ce0a1B5DceCebB5Dcac086Cd9F9e3eA5C", - "Alpine": "0x2Ef503950Be67a98746F484DA0bBAdA339DF3326" -} - - def _format_duration(duration_seconds: int) -> str: days, remainder = divmod(duration_seconds, 86400) hours, remainder = divmod(remainder, 3600) @@ -69,11 +65,11 @@ def _unstake_old_program( print(f"Checking if service is staked on {staking_program}...") # Check if service is staked - if staking_program.startswith("Everest"): + if staking_program.startswith("quickstart_alpha_everest"): if service_id not in get_service_ids(ledger_api, staking_contract_address): print(f"Service {service_id} is not staked on {staking_program}.") return - elif staking_program.startswith("Alpine"): + else: if not is_service_staked( ledger_api, service_id, staking_contract_address ): @@ -118,11 +114,16 @@ def _unstake_old_program( def _unstake_all_old_programs( - ledger_api: EthereumApi, service_id: int, owner_crypto: EthereumCrypto + ledger_api: EthereumApi, + service_id: int, + owner_crypto: EthereumCrypto, + current_staking_contract_address: str ) -> None: print("Unstaking from old programs...") - for program, address in OLD_STAKING_PROGRAMS.items(): - _unstake_old_program(ledger_api, service_id, address, program, owner_crypto) + for program_id, details in DEPRECATED_STAKING_PROGRAMS.items(): + staking_token_instance_address = details['deployment']['stakingTokenInstanceAddress'] + if staking_token_instance_address != current_staking_contract_address: + _unstake_old_program(ledger_api, service_id, staking_token_instance_address, program_id, owner_crypto) def _check_unstaking_availability( @@ -194,8 +195,6 @@ def _try_stake_service( def main() -> None: try: - staking_program = "Coastal" - print(f"Starting {Path(__file__).name} script ({staking_program})...\n") parser = argparse.ArgumentParser( description="Stake or unstake the service based on the state." @@ -229,12 +228,21 @@ def main() -> None: ) parser.add_argument("--password", type=str, help="Private key password") args = parser.parse_args() + + staking_program = args.staking_contract_address + print(f"Starting {Path(__file__).name} script ({staking_program})...\n") + ledger_api = EthereumApi(address=args.rpc) owner_crypto = EthereumCrypto( private_key_path=args.owner_private_key_path, password=args.password ) - _unstake_all_old_programs(ledger_api, args.service_id, owner_crypto) + _unstake_all_old_programs( + ledger_api=ledger_api, + service_id=args.service_id, + owner_crypto=owner_crypto, + current_staking_contract_address=args.staking_contract_address + ) # Collect information next_ts = get_next_checkpoint_ts(ledger_api, args.staking_contract_address) diff --git a/scripts/utils.py b/scripts/utils.py index 90f7d89..bb3dbc7 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -33,6 +33,7 @@ from packages.valory.contracts.service_staking_token.contract import ( ServiceStakingTokenContract, ) +from packages.valory.contracts.staking_token.contract import StakingTokenContract from autonomy.chain.tx import ( TxSettler, should_retry, @@ -46,9 +47,14 @@ TxBuildError, ) from autonomy.chain.config import ChainType - +from dotenv import dotenv_values +from choose_staking import ZERO_ADDRESS from packages.valory.skills.staking_abci.rounds import StakingState +from pathlib import Path +SCRIPT_PATH = Path(__file__).resolve().parent +STORE_PATH = Path(SCRIPT_PATH, "..", ".trader_runner") +DOTENV_PATH = Path(STORE_PATH, ".env") DEFAULT_ON_CHAIN_INTERACT_TIMEOUT = 120.0 DEFAULT_ON_CHAIN_INTERACT_RETRIES = 10 @@ -176,30 +182,46 @@ def is_service_staked( ledger_api: EthereumApi, service_id: int, staking_contract_address: str ) -> bool: """Check if service is staked.""" - service_staking_state = staking_contract.get_service_staking_state( - ledger_api, staking_contract_address, service_id - ).pop("data") + # TODO Not best approach. This is required because staking.py might call + # different contract versions. + for staking_contract in all_staking_contracts: + try: + service_staking_state = staking_contract.get_service_staking_state( + ledger_api, staking_contract_address, service_id + ).pop("data") - if isinstance(service_staking_state, int): - service_staking_state = StakingState(service_staking_state) + if isinstance(service_staking_state, int): + service_staking_state = StakingState(service_staking_state) - is_staked = service_staking_state == StakingState.STAKED or service_staking_state == StakingState.EVICTED - return is_staked + is_staked = service_staking_state == StakingState.STAKED or service_staking_state == StakingState.EVICTED + return is_staked + except: # noqa + continue + + raise Exception("Unable to retrieve staking state.") def is_service_evicted( ledger_api: EthereumApi, service_id: int, staking_contract_address: str ) -> bool: """Check if service is staked.""" - service_staking_state = staking_contract.get_service_staking_state( - ledger_api, staking_contract_address, service_id - ).pop("data") + # TODO Not best approach. This is required because staking.py might call + # different contract versions. + for staking_contract in all_staking_contracts: + try: + service_staking_state = staking_contract.get_service_staking_state( + ledger_api, staking_contract_address, service_id + ).pop("data") - if isinstance(service_staking_state, int): - service_staking_state = StakingState(service_staking_state) + if isinstance(service_staking_state, int): + service_staking_state = StakingState(service_staking_state) - is_evicted = service_staking_state == StakingState.EVICTED - return is_evicted + is_evicted = service_staking_state == StakingState.EVICTED + return is_evicted + except: # noqa + continue + + raise Exception("Unable to retrieve eviction state.") def get_next_checkpoint_ts( @@ -359,7 +381,29 @@ def send_tx_and_wait_for_receipt( return receipt -staking_contract = typing.cast( - typing.Type[ServiceStakingTokenContract], load_contract(ServiceStakingTokenContract) -) +# TODO 'staking_contract' refers to the current active program.abs +# There are methods above that will be called for other programs, +# and whose ABI might differ. A "patch" is currently implemented, +# but it should be refactored in a more elegant and robust way. +env_file_vars = dotenv_values(DOTENV_PATH) +activity_checker = env_file_vars.get("MECH_ACTIVITY_CHECKER_CONTRACT") + +if activity_checker is None or activity_checker == ZERO_ADDRESS: + staking_contract = typing.cast( + typing.Type[Contract], load_contract(ServiceStakingTokenContract) + ) +else: + staking_contract = typing.cast( + typing.Type[Contract], load_contract(StakingTokenContract) + ) + +all_staking_contracts = [ + typing.cast( + typing.Type[Contract], load_contract(ServiceStakingTokenContract) + ), + typing.cast( + typing.Type[Contract], load_contract(StakingTokenContract) + ) +] + erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) diff --git a/terminate_on_chain_service.sh b/terminate_on_chain_service.sh index d245b83..5a92d0f 100755 --- a/terminate_on_chain_service.sh +++ b/terminate_on_chain_service.sh @@ -94,6 +94,16 @@ validate_password() { fi } +export_dotenv() { + local dotenv_path="$1" + unamestr=$(uname) + if [ "$unamestr" = 'Linux' ]; then + export $(grep -v '^#' $dotenv_path | xargs -d '\n') + elif [ "$unamestr" = 'FreeBSD' ] || [ "$unamestr" = 'Darwin' ]; then + export $(grep -v '^#' $dotenv_path | xargs -0) + fi +} + store=".trader_runner" env_file_path="$store/.env" rpc_path="$store/rpc.txt" @@ -120,16 +130,13 @@ export RPC_TIMEOUT_SECONDS=120 export CUSTOM_CHAIN_RPC=$rpc export CUSTOM_CHAIN_ID=$gnosis_chain_id export CUSTOM_SERVICE_MANAGER_ADDRESS="0x04b0007b2aFb398015B76e5f22993a1fddF83644" -export CUSTOM_SERVICE_REGISTRY_ADDRESS="0x9338b5153AE39BB89f50468E608eD9d764B755fD" -export CUSTOM_STAKING_ADDRESS="0x43fB32f25dce34EB76c78C7A42C8F40F84BCD237" -export CUSTOM_OLAS_ADDRESS="0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" -export CUSTOM_SERVICE_REGISTRY_TOKEN_UTILITY_ADDRESS="0xa45E64d13A30a51b91ae0eb182e88a40e9b18eD8" export CUSTOM_GNOSIS_SAFE_PROXY_FACTORY_ADDRESS="0x3C1fF68f5aa342D296d4DEe4Bb1cACCA912D95fE" export CUSTOM_GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_ADDRESS="0x6e7f594f680f7aBad18b7a63de50F0FeE47dfD06" export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" -export MECH_AGENT_ADDRESS="0x77af31De935740567Cf4fF1986D04B2c964A786a" export WXDAI_ADDRESS="0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" +export_dotenv "$env_file_path" + set -e # Exit script on first error echo "--------------------------" echo "Terminate on-chain service"