From 04e2e5850e5c637b57b4e14ae0b5d35127b515f7 Mon Sep 17 00:00:00 2001 From: Ardian Date: Mon, 30 Oct 2023 22:15:29 +0100 Subject: [PATCH 01/10] feat: add `run_service_staking.sh` --- run_service_staking.sh | 752 +++++++++++++++++++++++++++++++++++++++++ scripts/approval.py | 170 ++++++++++ scripts/staking.py | 233 +++++++++++++ 3 files changed, 1155 insertions(+) create mode 100755 run_service_staking.sh create mode 100644 scripts/approval.py create mode 100644 scripts/staking.py diff --git a/run_service_staking.sh b/run_service_staking.sh new file mode 100755 index 00000000..26b7c089 --- /dev/null +++ b/run_service_staking.sh @@ -0,0 +1,752 @@ +#!/bin/bash + +# ------------------------------------------------------------------------------ +# +# Copyright 2023 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. +# +# ------------------------------------------------------------------------------ + +# Convert Hex to Dec +hex_to_decimal() { + $PYTHON_CMD -c "print(int('$1', 16))" +} + +# Convert Wei to Dai +wei_to_dai() { + local wei="$1" + local decimal_precision=4 # Change this to your desired precision + local dai=$($PYTHON_CMD -c "print('%.${decimal_precision}f' % ($wei / 1000000000000000000.0))") + echo "$dai" +} + +# Function to get the balance of an Ethereum address +get_balance() { + local address="$1" + curl -s -S -X POST \ + -H "Content-Type: application/json" \ + --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"$address\",\"latest\"],\"id\":1}" "$rpc" | \ + $PYTHON_CMD -c "import sys, json; print(json.load(sys.stdin)['result'])" +} + +# Function to ensure a minimum balance for an Ethereum address +ensure_minimum_balance() { + local address="$1" + local minimum_balance="$2" + local address_description="$3" + local wxdai="${4:-false}" + + wxdai_balance=0 + if [ "$wxdai" = "true" ] + then + wxdai_balance=$(poetry run python "../scripts/wxdai_balance.py" "$safe" "$rpc") + fi + + balance_hex=$(get_balance "$address") + balance=$(hex_to_decimal "$balance_hex") + balance=$($PYTHON_CMD -c "print(int($balance) + int($wxdai_balance))") + + echo "Checking balance of $address_description (minimum required $(wei_to_dai "$minimum_balance") DAI):" + echo " - Address: $address" + echo " - Balance: $(wei_to_dai "$balance") DAI" + + if [ "$($PYTHON_CMD -c "print($balance < $minimum_balance)")" == "True" ]; then + echo "" + echo " Please, fund address $address with at least $(wei_to_dai "$minimum_balance") DAI." + + local spin='-\|/' + local i=0 + local cycle_count=0 + while [ "$($PYTHON_CMD -c "print($balance < $minimum_balance)")" == "True" ]; do + printf "\r Waiting... %s" "${spin:$i:1} " + i=$(((i + 1) % 4)) + sleep .1 + + # This will be checked every 10 seconds (100 cycles). + cycle_count=$((cycle_count + 1)) + if [ "$cycle_count" -eq 100 ]; then + balance_hex=$(get_balance "$address") + balance=$(hex_to_decimal "$balance_hex") + balance=$((wxdai_balance+balance)) + cycle_count=0 + fi + done + + printf "\r Waiting... \n" + echo "" + echo " - Updated balance: $(wei_to_dai "$balance") DAI" + fi + + echo " OK." + echo "" +} + +# Get the address from a keys.json file +get_address() { + local keys_json_path="$1" + + if [ ! -f "$keys_json_path" ]; then + echo "Error: $keys_json_path does not exist." + return 1 + fi + + local address_start_position=17 + local address=$(sed -n 3p "$keys_json_path") + address=$(echo "$address" | + awk '{ print substr( $0, '$address_start_position', length($0) - '$address_start_position' - 1 ) }') + + echo -n "$address" +} + +# Get the private key from a keys.json file +get_private_key() { + local keys_json_path="$1" + + if [ ! -f "$keys_json_path" ]; then + echo "Error: $keys_json_path does not exist." + return 1 + fi + + local private_key_start_position=21 + local private_key=$(sed -n 4p "$keys_json_path") + private_key=$(echo -n "$private_key" | + awk '{ printf substr( $0, '$private_key_start_position', length($0) - '$private_key_start_position' ) }') + + private_key=$(echo -n "$private_key" | awk '{gsub(/\\"/, "\"", $0); print $0}') + private_key="${private_key#0x}" + + echo -n "$private_key" +} + +# Function to warm start the policy +warm_start() { + echo '["claude-prediction-offline", "claude-prediction-online", "deepmind-optimization", "deepmind-optimization-strong", "prediction-offline", "prediction-offline-sme", "prediction-online", "prediction-online-sme"]' >| sudo tee "$PWD/../$store/available_tools_store.json" + echo '{"counts": [23, 16, 54, 52, 20, 113, 33, 54], "eps": 0.1, "rewards": [0.21330030417382723, -0.06394516434480157, 0.5042296458897277, 0.38925697131774417, -0.2978133327512751, 1.055336834629253, -0.5935249657470777, 0.507192767958923]}' >| sudo tee "$PWD/../$store/policy_store.json" + echo '{}' >| sudo tee ".trader_runner/utilized_tools.json" +} + +# Function to add a volume to a service in a Docker Compose file +add_volume_to_service() { + local compose_file="$1" + local service_name="$2" + local volume_name="$3" + local volume_path="$4" + + # Check if the Docker Compose file exists + if [ ! -f "$compose_file" ]; then + echo "Docker Compose file '$compose_file' not found." + return 1 + fi + + # Check if the service exists in the Docker Compose file + if ! grep -q "^[[:space:]]*${service_name}:" "$compose_file"; then + echo "Service '$service_name' not found in '$compose_file'." + return 1 + fi + + if grep -q "^[[:space:]]*volumes:" "$compose_file"; then + awk -v volume_path="$volume_path" -v volume_name="$volume_name" ' + /^ *volumes:/ { + found_volumes = 1 + print + print " - " volume_path ":" volume_name ":Z" + next + } + 1 + ' "$compose_file" > temp_compose_file + else + awk -v service_name="$service_name" -v volume_path="$volume_path" -v volume_name="$volume_name" ' + /^ *'"$service_name"':/ { + found_service = 1 + print + print " volumes:" + print " - " volume_path ":" volume_name ":Z" + next + } + /^ *$/ && found_service == 1 { + print " volumes:" + print " - " volume_path ":" volume_name ":Z" + found_service = 0 + } + 1 + ' "$compose_file" > temp_compose_file + fi + + mv temp_compose_file "$compose_file" +} + +# Function to retrieve on-chain service state (requires env variables set to use --use-custom-chain) +get_on_chain_service_state() { + local service_id="$1" + local service_info=$(poetry run autonomy service --use-custom-chain info "$service_id") + local state="$(echo "$service_info" | awk '/Service State/ {sub(/\|[ \t]*Service State[ \t]*\|[ \t]*/, ""); sub(/[ \t]*\|[ \t]*/, ""); print}')" + echo "$state" +} + +# check the balances of the safe and the operator +check_balances() { + minimum_olas_balance=1000000000000000000 + output=$(poetry run python "../scripts/approval.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "../$operator_pkey_path" "$CUSTOM_OLAS_ADDRESS" "$minimum_olas_balance" "$rpc") + if [[ $? -ne 0 ]]; then + echo "Checking ERC20 balance and approval failed.\n$output" + exit 1 + fi + echo "$output" +} + +# stake or unstake a service +perform_staking_ops() { + local unstake="$1" + output=$(poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake") + if [[ $? -ne 0 ]]; then + echo "Swapping Safe owner failed.\n$output" + exit 1 + fi + echo "$output" +} + + +store=".trader_runner" +rpc_path="$store/rpc.txt" +operator_keys_file="$store/operator_keys.json" +operator_pkey_path="$store/operator_pkey.txt" +keys_json="keys.json" +keys_json_path="$store/$keys_json" +agent_pkey_path="$store/agent_pkey.txt" +agent_address_path="$store/agent_address.txt" +service_id_path="$store/service_id.txt" +service_safe_address_path="$store/service_safe_address.txt" +store_readme_path="$store/README.txt" + +# Function to create the .trader_runner storage +create_storage() { + local rpc="$1" + + echo "This is the first run of the script. The script will generate new operator and agent instance addresses." + echo "" + + mkdir "../$store" + + # Generate README.txt file + echo -e 'IMPORTANT:\n\n' \ + ' This folder contains crucial configuration information and autogenerated keys for your Trader agent.\n' \ + ' Please back up this folder and be cautious if you are modifying or sharing these files to avoid potential asset loss.' > "../$store_readme_path" + + # Generate the RPC file + echo -n "$rpc" > "../$rpc_path" + + # Generate the operator's key + poetry run autonomy generate-key -n1 ethereum + mv "$keys_json" "../$operator_keys_file" + operator_address=$(get_address "../$operator_keys_file") + operator_pkey=$(get_private_key "../$operator_keys_file") + echo -n "$operator_pkey" > "../$operator_pkey_path" + echo "Your operator's autogenerated public address: $operator_address" + echo "(The same address will be used as the service owner.)" + + # Generate the agent's key + poetry run autonomy generate-key -n1 ethereum + mv "$keys_json" "../$keys_json_path" + agent_address=$(get_address "../$keys_json_path") + agent_pkey=$(get_private_key "../$keys_json_path") + echo -n "$agent_pkey" > "../$agent_pkey_path" + echo -n "$agent_address" > "../$agent_address_path" + echo "Your agent instance's autogenerated public address: $agent_address" + echo "" +} + +# Function to read and load the .trader_runner storage information if it exists. +# Also sets `first_run` flag to identify whether we are running the script for the first time. +try_read_storage() { + if [ -d $store ]; then + + # INFO: This is a fix to avoid corrupting already-created stores + if [[ -f "$operator_keys_file" && ! -f "$operator_pkey_path" ]]; then + operator_pkey=$(get_private_key "$operator_keys_file") + echo -n "$operator_pkey" > "$operator_pkey_path" + fi + + # INFO: This is a fix to avoid corrupting already-created stores + if [[ -f "$keys_json_path" && ! -f "$agent_pkey_path" ]]; then + agent_pkey=$(get_private_key "$keys_json_path") + echo -n "$agent_pkey" > "$agent_pkey_path" + fi + + first_run=false + paths="$rpc_path $operator_keys_file $operator_pkey_path $keys_json_path $agent_address_path $agent_pkey_path $service_id_path" + + for file in $paths; do + if ! [ -f "$file" ]; then + if [ "$file" != $service_safe_address_path ] && [ "$file" != $service_id_path ]; then + echo "The runner's store is corrupted!" + echo "Please manually investigate the $store folder" + echo "Make sure that you do not lose your keys or any other important information!" + exit 1 + fi + fi + done + + rpc=$(cat $rpc_path) + agent_address=$(cat $agent_address_path) + operator_address=$(get_address "$operator_keys_file") + if [ -f "$service_id_path" ]; then + service_id=$(cat $service_id_path) + fi + else + first_run=true + fi +} + +# ------------------ +# Script starts here +# ------------------ + +set -e # Exit script on first error +echo "" +echo "---------------" +echo " Trader runner " +echo "---------------" +echo "" +echo "This script will assist you in setting up and running the Trader service (https://github.com/valory-xyz/trader)." +echo "" + +# Check if user is inside a venv +if [[ "$VIRTUAL_ENV" != "" ]] +then + echo "Please exit the virtual environment!" + exit 1 +fi + +# Check dependencies +if command -v python3 >/dev/null 2>&1; then + PYTHON_CMD="python3" +elif command -v python >/dev/null 2>&1; then + PYTHON_CMD="python" +else + echo >&2 "Python is not installed!"; + exit 1 +fi + +if [[ "$($PYTHON_CMD --version 2>&1)" != "Python 3.10."* ]] && [[ "$($PYTHON_CMD --version 2>&1)" != "Python 3.11."* ]]; then + echo >&2 "Python version >=3.10.0, <3.12.0 is required but found $($PYTHON_CMD --version 2>&1)"; + exit 1 +fi + +command -v git >/dev/null 2>&1 || +{ echo >&2 "Git is not installed!"; + exit 1 +} + +command -v poetry >/dev/null 2>&1 || +{ echo >&2 "Poetry is not installed!"; + exit 1 +} + +command -v docker >/dev/null 2>&1 || +{ echo >&2 "Docker is not installed!"; + exit 1 +} + +docker rm -f abci0 node0 trader_abci_0 trader_tm_0 &> /dev/null || +{ echo >&2 "Docker is not running!"; + exit 1 +} + +try_read_storage + +# Prompt for RPC +[[ -z "${rpc}" ]] && read -rsp "Enter a Gnosis RPC that supports eth_newFilter [hidden input]: " rpc && echo || rpc="${rpc}" + +# Check if eth_newFilter is supported +new_filter_supported=$(curl -s -S -X POST \ + -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_newFilter","params":["invalid"],"id":1}' "$rpc" | \ + $PYTHON_CMD -c "import sys, json; print(json.load(sys.stdin)['error']['message']=='The method eth_newFilter does not exist/is not available')") + +if [ "$new_filter_supported" = True ] +then + echo "The given RPC ($rpc) does not support 'eth_newFilter'! Terminating script..." + exit 1 +fi + +# clone repo +directory="trader" +# This is a tested version that works well. +# Feel free to replace this with a different version of the repo, but be careful as there might be breaking changes +service_version="feat/staking" +service_repo=https://github.com/valory-xyz/$directory.git +if [ -d $directory ] +then + echo "Detected an existing $directory repo. Using this one..." + echo "Please stop and manually delete the $directory repo if you updated the service's version ($service_version)!" + echo "You can run the following command, or continue with the pre-existing version of the service:" + echo "rm -r $directory" +else + echo "Cloning the $directory repo..." + git clone --depth 1 --branch $service_version $service_repo +fi + +cd $directory +if [ "$(git rev-parse --is-inside-work-tree)" = true ] +then + poetry install + poetry run autonomy packages sync +else + echo "$directory is not a git repo!" + exit 1 +fi + +if [ "$first_run" = "true" ] +then + create_storage "$rpc" +fi + +echo "" +echo "-----------------------------------------" +echo "Checking Autonolas Protocol service state" +echo "-----------------------------------------" + +gnosis_chain_id=100 +n_agents=1 + +# setup the minting tool +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="0x92499E80f50f06C4078794C179986907e7822Ea1" +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="0x3d77596beb0f130a4415df3D2D8232B3d3D31e44" +export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" +export AGENT_ID=12 + + +if [ -z ${service_id+x} ]; +then + # Check balances + suggested_amount=50000000000000000 + ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" + + echo "[Service owner] Minting your service on the Gnosis chain..." + + # create service + cost_of_bonding=10000000000000000 + nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" + service_id=$(poetry run autonomy mint \ + --skip-hash-check \ + --use-custom-chain \ + service packages/valory/services/$directory/ \ + --key "../$operator_pkey_path" \ + --nft $nft \ + -a $AGENT_ID \ + -n $n_agents \ + --threshold $n_agents \ + --token $CUSTOM_OLAS_ADDRESS \ + -c $cost_of_bonding + ) + # parse only the id from the response + service_id="${service_id##*: }" + # validate id + if ! [[ "$service_id" =~ ^[0-9]+$ || "$service_id" =~ ^[-][0-9]+$ ]] + then + echo "Service minting failed: $service_id" + exit 1 + fi + + echo -n "$service_id" > "../$service_id_path" +fi + +# Update the on-chain service if outdated +packages="packages/packages.json" +local_service_hash="$(grep 'service' $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") + +if [ "$local_service_hash" != "$remote_service_hash" ]; then + echo "" + echo "Your currently minted on-chain service (id $service_id) mismatches the fetched trader service ($service_version):" + echo " - Local service hash ($service_version): $local_service_hash" + echo " - On-chain service hash (id $service_id): $remote_service_hash" + echo "" + echo "This is most likely caused due to an update of the trader service code." + echo "The script will proceed now to update the on-chain service." + echo "The operator and agent addresses need to have enough funds so that the process is not interrupted." + echo "" + + echo "Warning: updating the on-chain may require that your service is unstaked." + echo "Continuing will automatically unstake your service if it is staked, which may effect your staking rewards." + echo "Do you want to continue? [y/N]" + read -r response + + if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then + echo "Skipping on-chain hash update." + else + # unstake the service + perform_staking_ops true + + # Check balances + suggested_amount=50000000000000000 + ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" + + suggested_amount=50000000000000000 + ensure_minimum_balance $agent_address $suggested_amount "agent instance's address" + + echo "------------------------------" + echo "Updating on-chain service $service_id" + echo "------------------------------" + echo "" + echo "PLEASE, DO NOT INTERRUPT THIS PROCESS." + echo "" + echo "Cancelling the on-chain service update prematurely could lead to an inconsistent state of the Safe or the on-chain service state, which may require manual intervention to resolve." + echo "" + + # TODO this condition should be increased to be service_state=DEPLOYED && current_safe_owner=agent_address. + # Otherwise the script will not recover the on-chain state in the (rare) case where this transaction succeeds but terminating transaction fails. + if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then + # transfer the ownership of the Safe from the agent to the service owner + # (in a live service, this should be done by sending a 0 DAI transfer to its Safe) + service_safe_address=$(<"../$service_safe_address_path") + echo "[Agent instance] Swapping Safe owner..." + output=$(poetry run python "../scripts/swap_safe_owner.py" "$service_safe_address" "../$agent_pkey_path" "$operator_address" "$rpc") + if [[ $? -ne 0 ]]; then + echo "Swapping Safe owner failed.\n$output" + exit 1 + fi + echo "$output" + fi + + # terminate current service + if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then + echo "[Service owner] Terminating on-chain service $service_id..." + output=$( + poetry run autonomy service \ + --use-custom-chain \ + terminate "$service_id" \ + --key "../$operator_pkey_path" + ) + if [[ $? -ne 0 ]]; then + echo "Terminating service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi + fi + + # unbond current service + if [ "$(get_on_chain_service_state "$service_id")" == "TERMINATED_BONDED" ]; then + echo "[Operator] Unbonding on-chain service $service_id..." + output=$( + poetry run autonomy service \ + --use-custom-chain \ + unbond "$service_id" \ + --key "../$operator_pkey_path" + ) + if [[ $? -ne 0 ]]; then + echo "Unbonding service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi + fi + + # update service + if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then + echo "[Service owner] Updating on-chain service $service_id..." + cost_of_bonding=10000000000000000 + nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" + output=$( + poetry run autonomy mint \ + --skip-hash-check \ + --use-custom-chain \ + service packages/valory/services/trader/ \ + --key "../$operator_pkey_path" \ + --nft $nft \ + -a $AGENT_ID \ + -n $n_agents \ + --threshold $n_agents \ + -c $cost_of_bonding \ + --update "$service_id" + ) + if [[ $? -ne 0 ]]; then + echo "Updating service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi + fi + + echo "" + echo "Finished updating on-chain service $service_id." + fi +fi + + +echo "" +echo "Ensuring on-chain service $service_id is in DEPLOYED state..." + +if [ "$(get_on_chain_service_state "$service_id")" != "DEPLOYED" ]; then + suggested_amount=25000000000000000 + ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" +fi + +# activate service +if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then + check_balances + echo "[Service owner] Activating registration for on-chain service $service_id..." + output=$(poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" "$service_id" --token $CUSTOM_OLAS_ADDRESS) + if [[ $? -ne 0 ]]; then + echo "Activating service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi +fi + +# register agent instance +if [ "$(get_on_chain_service_state "$service_id")" == "ACTIVE_REGISTRATION" ]; then + check_balances + echo "[Operator] Registering agent instance for on-chain service $service_id..." + output=$(poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address" --token $CUSTOM_OLAS_ADDRESS) + if [[ $? -ne 0 ]]; then + echo "Registering agent instance failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi +fi + +# deploy on-chain service +service_state="$(get_on_chain_service_state "$service_id")" +if [ "$service_state" == "FINISHED_REGISTRATION" ] && [ "$first_run" = "true" ]; then + echo "[Service owner] Deploying on-chain service $service_id..." + output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path") + if [[ $? -ne 0 ]]; then + echo "Deploying service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi +elif [ "$service_state" == "FINISHED_REGISTRATION" ]; then + echo "[Service owner] Deploying on-chain service $service_id..." + output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path" --reuse-multisig) + if [[ $? -ne 0 ]]; then + echo "Deploying service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi +fi + +# perform staking operations +# the following will stake the service in case it is not staked, and there are available rewards +# if the service is already staked, and there are no available rewards, it will unstake the service +perform_staking_ops + +# check state +service_state="$(get_on_chain_service_state "$service_id")" +if [ "$service_state" != "DEPLOYED" ]; then + echo "Something went wrong while deploying on-chain service. The service's state is $service_state." + echo "Please check the output of the script and the on-chain registry for more information." + exit 1 +fi + +echo "" +echo "Finished checking Autonolas Protocol service $service_id state." + + +echo "" +echo "------------------------------" +echo "Starting the trader service..." +echo "------------------------------" +echo "" + +# Get the deployed service's Safe address from the contract +service_info=$(poetry run autonomy service --use-custom-chain info "$service_id") +safe=$(echo "$service_info" | grep "Multisig Address") +address_start_position=31 +safe=$(echo "$safe" | + awk '{ print substr( $0, '$address_start_position', length($0) - '$address_start_position' - 3 ) }') +export SAFE_CONTRACT_ADDRESS=$safe +echo -n "$safe" > "../$service_safe_address_path" + +echo "Your agent instance's address: $agent_address" +echo "Your service's Safe address: $safe" +echo "" + +# Set environment variables. Tweak these to modify your strategy +export RPC_0="$rpc" +export CHAIN_ID=$gnosis_chain_id +export ALL_PARTICIPANTS='["'$agent_address'"]' +# This is the default market creator. Feel free to update with other market creators +export OMEN_CREATORS='["0x89c5cc945dd550BcFfb72Fe42BfF002429F46Fec"]' +export BET_AMOUNT_PER_THRESHOLD_000=0 +export BET_AMOUNT_PER_THRESHOLD_010=0 +export BET_AMOUNT_PER_THRESHOLD_020=0 +export BET_AMOUNT_PER_THRESHOLD_030=0 +export BET_AMOUNT_PER_THRESHOLD_040=0 +export BET_AMOUNT_PER_THRESHOLD_050=0 +export BET_AMOUNT_PER_THRESHOLD_060=0 +export BET_AMOUNT_PER_THRESHOLD_070=0 +export BET_AMOUNT_PER_THRESHOLD_080=30000000000000000 +export BET_AMOUNT_PER_THRESHOLD_090=80000000000000000 +export BET_AMOUNT_PER_THRESHOLD_100=100000000000000000 +export BET_THRESHOLD=5000000000000000 +export PROMPT_TEMPLATE="Please take over the role of a Data Scientist to evaluate the given question. With the given question \"@{question}\" and the \`yes\` option represented by \`@{yes}\` and the \`no\` option represented by \`@{no}\`, what are the respective probabilities of \`p_yes\` and \`p_no\` occurring?" +export REDEEM_MARGIN_DAYS=24 + +service_dir="trader_service" +build_dir="abci_build" +directory="$service_dir/$build_dir" + +suggested_amount=50000000000000000 +ensure_minimum_balance "$agent_address" $suggested_amount "agent instance's address" + +suggested_amount=500000000000000000 +ensure_minimum_balance "$SAFE_CONTRACT_ADDRESS" $suggested_amount "service Safe's address" "true" + +if [ -d $directory ] +then + echo "Detected an existing build. Using this one..." + cd $service_dir + + if rm -rf "$build_dir"; then + echo "Directory "$build_dir" removed successfully." + else + # If the above command fails, use sudo to remove + echo "You will need to provide sudo password in order for the script to delete part of the build artifacts." + sudo rm -rf "$build_dir" + echo "Directory "$build_dir" removed successfully." + fi +else + echo "Setting up the service..." + + if ! [ -d "$service_dir" ]; then + # Fetch the service + poetry run autonomy fetch --local --service valory/trader --alias $service_dir + fi + + cd $service_dir + # Build the image + poetry run autonomy build-image + cp ../../$keys_json_path $keys_json +fi + +# Build the deployment with a single agent +poetry run autonomy deploy build --n $n_agents -ltm + +cd .. + +warm_start +add_volume_to_service "$PWD/trader_service/abci_build/docker-compose.yaml" "trader_abci_0" "/data" "$PWD/../$store/" + +echo "Checking balances..." +check_balances + +# Run the deployment +poetry run autonomy deploy run --build-dir $directory --detach diff --git a/scripts/approval.py b/scripts/approval.py new file mode 100644 index 00000000..1f792430 --- /dev/null +++ b/scripts/approval.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 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 script swaps ownership of a Safe with a single owner.""" + +import argparse +import sys +import traceback +import typing +from pathlib import Path + +from aea.contracts.base import Contract +from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto +from eth_typing import HexStr + +from packages.valory.contracts.erc20.contract import ( + ERC20, +) +from packages.valory.contracts.service_staking_token.contract import ( + ServiceStakingTokenContract, +) + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +ZERO_ETH = 0 + +ContractType = typing.TypeVar("ContractType") + +GAS_PARAMS = { + "maxFeePerGas": 30_000_000_000, + "maxPriorityFeePerGas": 3_000_000_000, + "gas": 100_000, +} + +def load_contract(ctype: ContractType) -> ContractType: + """Load contract.""" + *parts, _ = ctype.__module__.split(".") + path = "/".join(parts) + return Contract.from_dir(directory=path) + + +def get_approval_tx(token: str, spender: str, amount: int) -> typing.Dict[str, typing.Any]: + """Get approval tx""" + approval_tx_data = erc20.build_approval_tx( + ledger_api, + token, + spender, + amount, + ).pop('data') + approval_tx = { + "data": approval_tx_data, + "to": token, + "value": ZERO_ETH, + } + return approval_tx + + +def get_balances(token: str, owner: str) -> typing.Tuple[int, int]: + """Returns the native and token balance of owner.""" + balances = erc20.check_balance(ledger_api, token, owner) + token_balance, native_balance = balances.pop("token"), balances.pop("wallet") + return token_balance, native_balance + + +def send_tx(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> str: + """Send transaction.""" + raw_tx = { + **raw_tx, + **GAS_PARAMS, + "from": crypto.address, + "nonce": ledger_api.api.eth.get_transaction_count(crypto.address), + "chainId": ledger_api.api.eth.chain_id, + } + signed_tx = crypto.sign_transaction(raw_tx) + tx_digest = typing.cast( + str, ledger_api.send_signed_transaction(signed_tx, raise_on_try=True) + ) + return tx_digest + + +def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + """Send transaction and wait for receipt.""" + tx_digest = HexStr(send_tx(crypto, raw_tx)) + receipt = ledger_api.api.eth.wait_for_transaction_receipt(tx_digest) + if receipt["status"] != 1: + raise ValueError("Transaction failed. Receipt:", receipt) + return receipt + + +if __name__ == "__main__": + try: + print(f" - Starting {Path(__file__).name} script...") + + parser = argparse.ArgumentParser( + description="Swap ownership of a Safe with a single owner on the Gnosis chain." + ) + parser.add_argument( + "service_id", + type=int, + help="The on-chain service id.", + ) + parser.add_argument( + "service_registry_address", + type=str, + help="The service registry contract address.", + ) + parser.add_argument( + "operator_private_key_path", + type=str, + help="Path to the file containing the service operator's Ethereum private key", + ) + parser.add_argument( + "olas_address", + type=str, + help="The address of the OLAS token.", + ) + parser.add_argument( + "minimum_olas_balance", + type=int, + help="The minimum OLAS balance required for agent registration.", + ) + parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") + args = parser.parse_args() + + ledger_api = EthereumApi(address=args.rpc) + owner_crypto = EthereumCrypto( + private_key_path=args.operator_private_key_path + ) + staking_contract = typing.cast( + typing.Type[ServiceStakingTokenContract], + load_contract(ServiceStakingTokenContract) + ) + erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) + allowance = erc20.get_allowance(ledger_api, args.olas_address, owner_crypto.address, args.service_registry_address).pop('data') + if allowance >= args.minimum_olas_balance: + print("Operator has sufficient OLAS allowance.") + sys.exit(0) + + token_balance, native_balance = get_balances(args.olas_address, owner_crypto.address) + if token_balance < args.minimum_olas_balance: + raise ValueError(f"Operator has insufficient OLAS balance. Required: {args.minimum_olas_balance}, Actual: {token_balance}") + + if native_balance == 0: + raise ValueError("Operator has no xDAI.") + + approval_tx = get_approval_tx(args.olas_address, args.service_registry_address, args.minimum_olas_balance) + send_tx_and_wait_for_receipt(owner_crypto, approval_tx) + print("Approved service registry to spend OLAS.") + sys.exit(0) + + except Exception as e: # pylint: disable=broad-except + print(f"An error occurred while executing {Path(__file__).name}: {str(e)}") + traceback.print_exc() + sys.exit(1) diff --git a/scripts/staking.py b/scripts/staking.py new file mode 100644 index 00000000..5e2468bb --- /dev/null +++ b/scripts/staking.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2022-2023 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 script swaps ownership of a Safe with a single owner.""" + +import argparse +import sys +import traceback +import typing +from pathlib import Path + +from aea.contracts.base import Contract +from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto +from eth_typing import HexStr + +from packages.valory.contracts.erc20.contract import ( + ERC20, +) +from packages.valory.contracts.service_staking_token.contract import ( + ServiceStakingTokenContract, +) + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +ZERO_ETH = 0 + +ContractType = typing.TypeVar("ContractType") + +GAS_PARAMS = { + "maxFeePerGas": 30_000_000_000, + "maxPriorityFeePerGas": 3_000_000_000, + "gas": 100_000, +} + + +def load_contract(ctype: ContractType) -> ContractType: + """Load contract.""" + *parts, _ = ctype.__module__.split(".") + path = "/".join(parts) + return Contract.from_dir(directory=path) + + +def get_stake_txs(service_id: int, service_registry_address: str, staking_contract_address: str) -> typing.List: + """Stake the service""" + # 1. approve the service to make use of the + + # we make use of the ERC20 contract to build the approval transaction + # since it has the same interface as ERC721 + # we use the ZERO_ADDRESS as the contract address since we don't do any contract interaction here, + # we are simply encoding + approval_tx = get_approval_tx(service_id, service_registry_address, staking_contract_address) + + # 2. stake the service + staking_contract = typing.cast(typing.Type[ServiceStakingTokenContract], load_contract(ServiceStakingTokenContract)) + stake_tx_data = staking_contract.build_stake_tx(ledger_api, staking_contract_address, service_id).pop('data') + stake_tx = { + "data": stake_tx_data, + "to": staking_contract_address, + "value": ZERO_ETH, + } + return [approval_tx, stake_tx] + + +def get_approval_tx(service_id, service_registry_address, staking_contract_address): + """Get approval tx""" + approval_tx_data = erc20.build_approval_tx(ledger_api, service_registry_address, staking_contract_address, + service_id).pop('data') + approval_tx = { + "data": approval_tx_data, + "to": service_registry_address, + "value": ZERO_ETH, + } + return approval_tx + + +def get_unstake_txs(service_id: int, staking_contract_address: str) -> typing.List: + """Get unstake txs""" + + unstake_tx_data = staking_contract.build_unstake_tx(ledger_api, staking_contract_address, service_id).pop('data') + unstake_tx = { + "data": unstake_tx_data, + "to": staking_contract_address, + "value": ZERO_ETH, + } + return [unstake_tx] + + +def get_available_rewards(staking_contract_address: str) -> int: + """Get available rewards.""" + rewards = staking_contract.available_rewards(ledger_api, staking_contract_address).pop('data') + return rewards + + +def is_service_staked(service_id: int, staking_contract_address: str) -> bool: + """Check if service is staked.""" + is_staked = staking_contract.is_service_staked(ledger_api, staking_contract_address, service_id).pop('data') + return is_staked + + +def send_tx(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> str: + """Send transaction.""" + raw_tx = { + **raw_tx, + **GAS_PARAMS, + "from": crypto.address, + "nonce": ledger_api.api.eth.get_transaction_count(crypto.address), + "chainId": ledger_api.api.eth.chain_id, + } + signed_tx = crypto.sign_transaction(raw_tx) + tx_digest = typing.cast( + str, ledger_api.send_signed_transaction(signed_tx, raise_on_try=True) + ) + return tx_digest + + +def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + """Send transaction and wait for receipt.""" + tx_digest = HexStr(send_tx(crypto, raw_tx)) + receipt = ledger_api.api.eth.wait_for_transaction_receipt(tx_digest) + if receipt["status"] != 1: + raise ValueError("Transaction failed. Receipt:", receipt) + return receipt + + +if __name__ == "__main__": + try: + print(f" - Starting {Path(__file__).name} script...") + + parser = argparse.ArgumentParser( + description="Stake or unstake the service based on the state." + ) + parser.add_argument( + "service_id", + type=int, + help="The on-chain service id.", + ) + parser.add_argument( + "service_registry_address", + type=str, + help="The service registry contract address.", + ) + parser.add_argument( + "staking_contract_address", + type=str, + help="The staking contract address.", + ) + parser.add_argument( + "owner_private_key_path", + type=str, + help="Path to the file containing the service owner's Ethereum private key", + ) + parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") + parser.add_argument( + "unstake", + type=bool, + help="True if the service should be unstaked, False if it should be staked", + default=False, + ) + args = parser.parse_args() + + ledger_api = EthereumApi(address=args.rpc) + owner_crypto = EthereumCrypto( + private_key_path=args.owner_private_key_path + ) + staking_contract = typing.cast( + typing.Type[ServiceStakingTokenContract], + load_contract(ServiceStakingTokenContract) + ) + erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) + if args.unstake: + if not is_service_staked(args.service_id, args.staking_contract_address): + # the service is not staked, so we don't need to do anything + print(f"Service {args.service_id} is not staked. Exiting...") + sys.exit(0) + + print(f"Unstaking service {args.service_id}") + unstake_txs = get_unstake_txs(args.service_id, args.staking_contract_address) + for tx in unstake_txs: + send_tx_and_wait_for_receipt(owner_crypto, tx) + print("Successfully unstaked.") + sys.exit(0) + + if is_service_staked(args.service_id, args.staking_contract_address): + print( + f"Service {args.service_id} is already staked. " + f"Checking if the staking contract has any rewards..." + ) + available_rewards = get_available_rewards(args.staking_contract_address) + if available_rewards == 0: + print("No rewards available. Unstaking...") + unstake_txs = get_unstake_txs(args.service_id, args.staking_contract_address) + for tx in unstake_txs: + send_tx_and_wait_for_receipt(owner_crypto, tx) + + print("Unstaked successfully.") + sys.exit(0) + + print("There are rewards available. The service should remain staked.") + sys.exit(0) + + print(f"Service {args.service_id} is not staked. Checking for available rewards...") + available_rewards = get_available_rewards(args.staking_contract_address) + if available_rewards == 0: + # no rewards available, do nothing + print("No rewards available. The service cannot be staked.") + sys.exit(0) + + print(f"Rewards available: {available_rewards}. Staking the service...") + stake_txs = get_stake_txs(args.service_id, args.service_registry_address, args.staking_contract_address) + for tx in stake_txs: + send_tx_and_wait_for_receipt(owner_crypto, tx) + + print(f"Service {args.service_id} staked successfully.") + except Exception as e: # pylint: disable=broad-except + print(f"An error occurred while executing {Path(__file__).name}: {str(e)}") + traceback.print_exc() + sys.exit(1) From 6e84d54838da40b21cf3e8376b54ea6f2981be68 Mon Sep 17 00:00:00 2001 From: Ardian Date: Tue, 31 Oct 2023 13:45:07 +0100 Subject: [PATCH 02/10] feat: add liveness checks --- run_service_staking.sh | 13 ++++++------- scripts/staking.py | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/run_service_staking.sh b/run_service_staking.sh index 26b7c089..c7a60ff9 100755 --- a/run_service_staking.sh +++ b/run_service_staking.sh @@ -208,7 +208,7 @@ check_balances() { # stake or unstake a service perform_staking_ops() { local unstake="$1" - output=$(poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake") + output=$(poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" "$SKIP_LIVENESS_CHECK") if [[ $? -ne 0 ]]; then echo "Swapping Safe owner failed.\n$output" exit 1 @@ -443,7 +443,7 @@ then echo "[Service owner] Minting your service on the Gnosis chain..." # create service - cost_of_bonding=10000000000000000 + cost_of_bonding=1000000000000000000 nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" service_id=$(poetry run autonomy mint \ --skip-hash-check \ @@ -563,7 +563,7 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then # update service if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then echo "[Service owner] Updating on-chain service $service_id..." - cost_of_bonding=10000000000000000 + cost_of_bonding=1000000000000000000 nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" output=$( poetry run autonomy mint \ @@ -575,6 +575,7 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then -a $AGENT_ID \ -n $n_agents \ --threshold $n_agents \ + --token $CUSTOM_OLAS_ADDRESS \ -c $cost_of_bonding \ --update "$service_id" ) @@ -615,6 +616,7 @@ fi if [ "$(get_on_chain_service_state "$service_id")" == "ACTIVE_REGISTRATION" ]; then check_balances echo "[Operator] Registering agent instance for on-chain service $service_id..." + cost_of_bonding=1000000000000000000 output=$(poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address" --token $CUSTOM_OLAS_ADDRESS) if [[ $? -ne 0 ]]; then echo "Registering agent instance failed.\n$output" @@ -635,7 +637,7 @@ if [ "$service_state" == "FINISHED_REGISTRATION" ] && [ "$first_run" = "true" ]; fi elif [ "$service_state" == "FINISHED_REGISTRATION" ]; then echo "[Service owner] Deploying on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path" --reuse-multisig) + output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path") if [[ $? -ne 0 ]]; then echo "Deploying service failed.\n$output" echo "Please, delete or rename the ./trader folder and try re-run this script again." @@ -745,8 +747,5 @@ cd .. warm_start add_volume_to_service "$PWD/trader_service/abci_build/docker-compose.yaml" "trader_abci_0" "/data" "$PWD/../$store/" -echo "Checking balances..." -check_balances - # Run the deployment poetry run autonomy deploy run --build-dir $directory --detach diff --git a/scripts/staking.py b/scripts/staking.py index 5e2468bb..71057eeb 100644 --- a/scripts/staking.py +++ b/scripts/staking.py @@ -22,6 +22,7 @@ import argparse import sys +import time import traceback import typing from pathlib import Path @@ -45,7 +46,7 @@ GAS_PARAMS = { "maxFeePerGas": 30_000_000_000, "maxPriorityFeePerGas": 3_000_000_000, - "gas": 100_000, + "gas": 500_000, } @@ -92,13 +93,21 @@ def get_approval_tx(service_id, service_registry_address, staking_contract_addre def get_unstake_txs(service_id: int, staking_contract_address: str) -> typing.List: """Get unstake txs""" + checkpoint_tx_data = staking_contract.build_checkpoint_tx(ledger_api, staking_contract_address).pop('data') + checkpoint_tx = { + "data": checkpoint_tx_data, + "to": staking_contract_address, + "value": ZERO_ETH, + } + unstake_tx_data = staking_contract.build_unstake_tx(ledger_api, staking_contract_address, service_id).pop('data') unstake_tx = { "data": unstake_tx_data, "to": staking_contract_address, "value": ZERO_ETH, } - return [unstake_tx] + + return [checkpoint_tx, unstake_tx] def get_available_rewards(staking_contract_address: str) -> int: @@ -113,6 +122,18 @@ def is_service_staked(service_id: int, staking_contract_address: str) -> bool: return is_staked +def get_next_checkpoint_ts(service_id: int, staking_contract_address: str) -> int: + """Check if service is staked.""" + checkpoint_ts = staking_contract.get_next_checkpoint_ts(ledger_api, staking_contract_address, service_id).pop('data') + return checkpoint_ts + + +def get_staking_rewards(service_id: int, staking_contract_address: str) -> int: + """Check if service is staked.""" + rewards = staking_contract.get_staking_rewards(ledger_api, staking_contract_address, service_id).pop('data') + return rewards + + def send_tx(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> str: """Send transaction.""" raw_tx = { @@ -172,6 +193,12 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str help="True if the service should be unstaked, False if it should be staked", default=False, ) + parser.add_argument( + "skip_livenesss_check", + type=bool, + help="Set to true to skip the liveness check, note that this might end up causing you to lose staking rewards.", + default=False, + ) args = parser.parse_args() ledger_api = EthereumApi(address=args.rpc) @@ -189,6 +216,16 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str print(f"Service {args.service_id} is not staked. Exiting...") sys.exit(0) + next_ts = get_next_checkpoint_ts(args.service_id, args.staking_contract_address) + staking_rewards = get_staking_rewards(args.service_id, args.staking_contract_address) + if staking_rewards == 0 and next_ts > time.time() and not args.skip_livenesss_check: + print( + f"The liveness period has not passed. " + f"If you want to unstake anyway, " + f"run the script by running SKIP_LIVENESS_CHECK=true." + ) + sys.exit(1) + print(f"Unstaking service {args.service_id}") unstake_txs = get_unstake_txs(args.service_id, args.staking_contract_address) for tx in unstake_txs: From 04eff906158ba7f06e61a3da8233083446838292 Mon Sep 17 00:00:00 2001 From: Ardian Date: Tue, 31 Oct 2023 14:03:04 +0100 Subject: [PATCH 03/10] chore: simplify livesness check --- run_service_staking.sh | 2 +- scripts/staking.py | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/run_service_staking.sh b/run_service_staking.sh index c7a60ff9..5b219a22 100755 --- a/run_service_staking.sh +++ b/run_service_staking.sh @@ -208,7 +208,7 @@ check_balances() { # stake or unstake a service perform_staking_ops() { local unstake="$1" - output=$(poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" "$SKIP_LIVENESS_CHECK") + output=$(poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" "$SKIP_LAST_EPOCH_REWARDS") if [[ $? -ne 0 ]]; then echo "Swapping Safe owner failed.\n$output" exit 1 diff --git a/scripts/staking.py b/scripts/staking.py index 71057eeb..1fb8ea68 100644 --- a/scripts/staking.py +++ b/scripts/staking.py @@ -93,13 +93,6 @@ def get_approval_tx(service_id, service_registry_address, staking_contract_addre def get_unstake_txs(service_id: int, staking_contract_address: str) -> typing.List: """Get unstake txs""" - checkpoint_tx_data = staking_contract.build_checkpoint_tx(ledger_api, staking_contract_address).pop('data') - checkpoint_tx = { - "data": checkpoint_tx_data, - "to": staking_contract_address, - "value": ZERO_ETH, - } - unstake_tx_data = staking_contract.build_unstake_tx(ledger_api, staking_contract_address, service_id).pop('data') unstake_tx = { "data": unstake_tx_data, @@ -107,7 +100,7 @@ def get_unstake_txs(service_id: int, staking_contract_address: str) -> typing.Li "value": ZERO_ETH, } - return [checkpoint_tx, unstake_tx] + return [unstake_tx] def get_available_rewards(staking_contract_address: str) -> int: @@ -217,12 +210,11 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str sys.exit(0) next_ts = get_next_checkpoint_ts(args.service_id, args.staking_contract_address) - staking_rewards = get_staking_rewards(args.service_id, args.staking_contract_address) - if staking_rewards == 0 and next_ts > time.time() and not args.skip_livenesss_check: + if next_ts > time.time() and not args.skip_livenesss_check: print( f"The liveness period has not passed. " f"If you want to unstake anyway, " - f"run the script by running SKIP_LIVENESS_CHECK=true." + f"run the script by running with SKIP_LAST_EPOCH_REWARDS=true." ) sys.exit(1) From e58df6f0bb8b37aecf00dc77cfa404493ea6250c Mon Sep 17 00:00:00 2001 From: Ardian Date: Tue, 31 Oct 2023 16:33:22 +0100 Subject: [PATCH 04/10] chore: export mech address --- run_service_staking.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/run_service_staking.sh b/run_service_staking.sh index 5b219a22..ec9e69f3 100755 --- a/run_service_staking.sh +++ b/run_service_staking.sh @@ -432,6 +432,7 @@ export CUSTOM_GNOSIS_SAFE_PROXY_FACTORY_ADDRESS="0x3C1fF68f5aa342D296d4DEe4Bb1cA export CUSTOM_GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_ADDRESS="0x3d77596beb0f130a4415df3D2D8232B3d3D31e44" export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" export AGENT_ID=12 +export MECH_AGENT_ADDRESS="0x77af31De935740567Cf4fF1986D04B2c964A786a" if [ -z ${service_id+x} ]; From decf5e5aac05b93ec1bdf5bd9f6769ea0aae5543 Mon Sep 17 00:00:00 2001 From: Ardian Date: Tue, 31 Oct 2023 16:49:45 +0100 Subject: [PATCH 05/10] fix: check olas balances --- scripts/approval.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/approval.py b/scripts/approval.py index 1f792430..38a1abe5 100644 --- a/scripts/approval.py +++ b/scripts/approval.py @@ -147,11 +147,6 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str load_contract(ServiceStakingTokenContract) ) erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) - allowance = erc20.get_allowance(ledger_api, args.olas_address, owner_crypto.address, args.service_registry_address).pop('data') - if allowance >= args.minimum_olas_balance: - print("Operator has sufficient OLAS allowance.") - sys.exit(0) - token_balance, native_balance = get_balances(args.olas_address, owner_crypto.address) if token_balance < args.minimum_olas_balance: raise ValueError(f"Operator has insufficient OLAS balance. Required: {args.minimum_olas_balance}, Actual: {token_balance}") @@ -159,6 +154,11 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str if native_balance == 0: raise ValueError("Operator has no xDAI.") + allowance = erc20.get_allowance(ledger_api, args.olas_address, owner_crypto.address, args.service_registry_address).pop('data') + if allowance >= args.minimum_olas_balance: + print("Operator has sufficient OLAS allowance.") + sys.exit(0) + approval_tx = get_approval_tx(args.olas_address, args.service_registry_address, args.minimum_olas_balance) send_tx_and_wait_for_receipt(owner_crypto, approval_tx) print("Approved service registry to spend OLAS.") From e2713e419ff11842a99cd53a09214546ce77fa20 Mon Sep 17 00:00:00 2001 From: Ardian Date: Tue, 31 Oct 2023 17:28:42 +0100 Subject: [PATCH 06/10] chore: introduced utils --- scripts/approval.py | 110 +++++----------------- scripts/staking.py | 171 ++++++---------------------------- scripts/utils.py | 219 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 229 deletions(-) create mode 100644 scripts/utils.py diff --git a/scripts/approval.py b/scripts/approval.py index 38a1abe5..ccedceec 100644 --- a/scripts/approval.py +++ b/scripts/approval.py @@ -23,85 +23,16 @@ import argparse import sys import traceback -import typing from pathlib import Path -from aea.contracts.base import Contract from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto -from eth_typing import HexStr -from packages.valory.contracts.erc20.contract import ( - ERC20, +from utils import ( + get_balances, + send_tx_and_wait_for_receipt, + get_allowance, + get_approval_tx, ) -from packages.valory.contracts.service_staking_token.contract import ( - ServiceStakingTokenContract, -) - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -ZERO_ETH = 0 - -ContractType = typing.TypeVar("ContractType") - -GAS_PARAMS = { - "maxFeePerGas": 30_000_000_000, - "maxPriorityFeePerGas": 3_000_000_000, - "gas": 100_000, -} - -def load_contract(ctype: ContractType) -> ContractType: - """Load contract.""" - *parts, _ = ctype.__module__.split(".") - path = "/".join(parts) - return Contract.from_dir(directory=path) - - -def get_approval_tx(token: str, spender: str, amount: int) -> typing.Dict[str, typing.Any]: - """Get approval tx""" - approval_tx_data = erc20.build_approval_tx( - ledger_api, - token, - spender, - amount, - ).pop('data') - approval_tx = { - "data": approval_tx_data, - "to": token, - "value": ZERO_ETH, - } - return approval_tx - - -def get_balances(token: str, owner: str) -> typing.Tuple[int, int]: - """Returns the native and token balance of owner.""" - balances = erc20.check_balance(ledger_api, token, owner) - token_balance, native_balance = balances.pop("token"), balances.pop("wallet") - return token_balance, native_balance - - -def send_tx(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> str: - """Send transaction.""" - raw_tx = { - **raw_tx, - **GAS_PARAMS, - "from": crypto.address, - "nonce": ledger_api.api.eth.get_transaction_count(crypto.address), - "chainId": ledger_api.api.eth.chain_id, - } - signed_tx = crypto.sign_transaction(raw_tx) - tx_digest = typing.cast( - str, ledger_api.send_signed_transaction(signed_tx, raise_on_try=True) - ) - return tx_digest - - -def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: - """Send transaction and wait for receipt.""" - tx_digest = HexStr(send_tx(crypto, raw_tx)) - receipt = ledger_api.api.eth.wait_for_transaction_receipt(tx_digest) - if receipt["status"] != 1: - raise ValueError("Transaction failed. Receipt:", receipt) - return receipt - if __name__ == "__main__": try: @@ -139,28 +70,35 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str args = parser.parse_args() ledger_api = EthereumApi(address=args.rpc) - owner_crypto = EthereumCrypto( - private_key_path=args.operator_private_key_path - ) - staking_contract = typing.cast( - typing.Type[ServiceStakingTokenContract], - load_contract(ServiceStakingTokenContract) + owner_crypto = EthereumCrypto(private_key_path=args.operator_private_key_path) + token_balance, native_balance = get_balances( + ledger_api, args.olas_address, owner_crypto.address ) - erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) - token_balance, native_balance = get_balances(args.olas_address, owner_crypto.address) if token_balance < args.minimum_olas_balance: - raise ValueError(f"Operator has insufficient OLAS balance. Required: {args.minimum_olas_balance}, Actual: {token_balance}") + raise ValueError( + f"Operator has insufficient OLAS balance. Required: {args.minimum_olas_balance}, Actual: {token_balance}" + ) if native_balance == 0: raise ValueError("Operator has no xDAI.") - allowance = erc20.get_allowance(ledger_api, args.olas_address, owner_crypto.address, args.service_registry_address).pop('data') + allowance = get_allowance( + ledger_api, + args.olas_address, + owner_crypto.address, + args.service_registry_address, + ) if allowance >= args.minimum_olas_balance: print("Operator has sufficient OLAS allowance.") sys.exit(0) - approval_tx = get_approval_tx(args.olas_address, args.service_registry_address, args.minimum_olas_balance) - send_tx_and_wait_for_receipt(owner_crypto, approval_tx) + approval_tx = get_approval_tx( + ledger_api, + args.olas_address, + args.service_registry_address, + args.minimum_olas_balance, + ) + send_tx_and_wait_for_receipt(ledger_api, owner_crypto, approval_tx) print("Approved service registry to spend OLAS.") sys.exit(0) diff --git a/scripts/staking.py b/scripts/staking.py index 1fb8ea68..829e690b 100644 --- a/scripts/staking.py +++ b/scripts/staking.py @@ -24,133 +24,13 @@ import sys import time import traceback -import typing from pathlib import Path -from aea.contracts.base import Contract from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto -from eth_typing import HexStr -from packages.valory.contracts.erc20.contract import ( - ERC20, -) -from packages.valory.contracts.service_staking_token.contract import ( - ServiceStakingTokenContract, -) - -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -ZERO_ETH = 0 - -ContractType = typing.TypeVar("ContractType") - -GAS_PARAMS = { - "maxFeePerGas": 30_000_000_000, - "maxPriorityFeePerGas": 3_000_000_000, - "gas": 500_000, -} - - -def load_contract(ctype: ContractType) -> ContractType: - """Load contract.""" - *parts, _ = ctype.__module__.split(".") - path = "/".join(parts) - return Contract.from_dir(directory=path) - - -def get_stake_txs(service_id: int, service_registry_address: str, staking_contract_address: str) -> typing.List: - """Stake the service""" - # 1. approve the service to make use of the - - # we make use of the ERC20 contract to build the approval transaction - # since it has the same interface as ERC721 - # we use the ZERO_ADDRESS as the contract address since we don't do any contract interaction here, - # we are simply encoding - approval_tx = get_approval_tx(service_id, service_registry_address, staking_contract_address) - - # 2. stake the service - staking_contract = typing.cast(typing.Type[ServiceStakingTokenContract], load_contract(ServiceStakingTokenContract)) - stake_tx_data = staking_contract.build_stake_tx(ledger_api, staking_contract_address, service_id).pop('data') - stake_tx = { - "data": stake_tx_data, - "to": staking_contract_address, - "value": ZERO_ETH, - } - return [approval_tx, stake_tx] - - -def get_approval_tx(service_id, service_registry_address, staking_contract_address): - """Get approval tx""" - approval_tx_data = erc20.build_approval_tx(ledger_api, service_registry_address, staking_contract_address, - service_id).pop('data') - approval_tx = { - "data": approval_tx_data, - "to": service_registry_address, - "value": ZERO_ETH, - } - return approval_tx - - -def get_unstake_txs(service_id: int, staking_contract_address: str) -> typing.List: - """Get unstake txs""" - - unstake_tx_data = staking_contract.build_unstake_tx(ledger_api, staking_contract_address, service_id).pop('data') - unstake_tx = { - "data": unstake_tx_data, - "to": staking_contract_address, - "value": ZERO_ETH, - } - - return [unstake_tx] - - -def get_available_rewards(staking_contract_address: str) -> int: - """Get available rewards.""" - rewards = staking_contract.available_rewards(ledger_api, staking_contract_address).pop('data') - return rewards - - -def is_service_staked(service_id: int, staking_contract_address: str) -> bool: - """Check if service is staked.""" - is_staked = staking_contract.is_service_staked(ledger_api, staking_contract_address, service_id).pop('data') - return is_staked - - -def get_next_checkpoint_ts(service_id: int, staking_contract_address: str) -> int: - """Check if service is staked.""" - checkpoint_ts = staking_contract.get_next_checkpoint_ts(ledger_api, staking_contract_address, service_id).pop('data') - return checkpoint_ts - - -def get_staking_rewards(service_id: int, staking_contract_address: str) -> int: - """Check if service is staked.""" - rewards = staking_contract.get_staking_rewards(ledger_api, staking_contract_address, service_id).pop('data') - return rewards - - -def send_tx(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> str: - """Send transaction.""" - raw_tx = { - **raw_tx, - **GAS_PARAMS, - "from": crypto.address, - "nonce": ledger_api.api.eth.get_transaction_count(crypto.address), - "chainId": ledger_api.api.eth.chain_id, - } - signed_tx = crypto.sign_transaction(raw_tx) - tx_digest = typing.cast( - str, ledger_api.send_signed_transaction(signed_tx, raise_on_try=True) - ) - return tx_digest - - -def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: - """Send transaction and wait for receipt.""" - tx_digest = HexStr(send_tx(crypto, raw_tx)) - receipt = ledger_api.api.eth.wait_for_transaction_receipt(tx_digest) - if receipt["status"] != 1: - raise ValueError("Transaction failed. Receipt:", receipt) - return receipt +from utils import is_service_staked, get_next_checkpoint_ts, get_unstake_txs, send_tx_and_wait_for_receipt, \ + get_available_rewards, get_stake_txs if __name__ == "__main__": try: @@ -193,23 +73,17 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str default=False, ) args = parser.parse_args() - ledger_api = EthereumApi(address=args.rpc) - owner_crypto = EthereumCrypto( - private_key_path=args.owner_private_key_path - ) - staking_contract = typing.cast( - typing.Type[ServiceStakingTokenContract], - load_contract(ServiceStakingTokenContract) - ) - erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) + owner_crypto = EthereumCrypto(private_key_path=args.owner_private_key_path) if args.unstake: - if not is_service_staked(args.service_id, args.staking_contract_address): + if not is_service_staked(ledger_api, args.service_id, args.staking_contract_address): # the service is not staked, so we don't need to do anything print(f"Service {args.service_id} is not staked. Exiting...") sys.exit(0) - next_ts = get_next_checkpoint_ts(args.service_id, args.staking_contract_address) + next_ts = get_next_checkpoint_ts( + ledger_api, args.service_id, args.staking_contract_address + ) if next_ts > time.time() and not args.skip_livenesss_check: print( f"The liveness period has not passed. " @@ -219,23 +93,27 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str sys.exit(1) print(f"Unstaking service {args.service_id}") - unstake_txs = get_unstake_txs(args.service_id, args.staking_contract_address) + unstake_txs = get_unstake_txs( + ledger_api, args.service_id, args.staking_contract_address + ) for tx in unstake_txs: - send_tx_and_wait_for_receipt(owner_crypto, tx) + send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx) print("Successfully unstaked.") sys.exit(0) - if is_service_staked(args.service_id, args.staking_contract_address): + if is_service_staked(ledger_api, args.service_id, args.staking_contract_address): print( f"Service {args.service_id} is already staked. " f"Checking if the staking contract has any rewards..." ) - available_rewards = get_available_rewards(args.staking_contract_address) + available_rewards = get_available_rewards(ledger_api, args.staking_contract_address) if available_rewards == 0: print("No rewards available. Unstaking...") - unstake_txs = get_unstake_txs(args.service_id, args.staking_contract_address) + unstake_txs = get_unstake_txs( + ledger_api, args.service_id, args.staking_contract_address + ) for tx in unstake_txs: - send_tx_and_wait_for_receipt(owner_crypto, tx) + send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx) print("Unstaked successfully.") sys.exit(0) @@ -243,17 +121,24 @@ def send_tx_and_wait_for_receipt(crypto: EthereumCrypto, raw_tx: typing.Dict[str print("There are rewards available. The service should remain staked.") sys.exit(0) - print(f"Service {args.service_id} is not staked. Checking for available rewards...") - available_rewards = get_available_rewards(args.staking_contract_address) + print( + f"Service {args.service_id} is not staked. Checking for available rewards..." + ) + available_rewards = get_available_rewards(ledger_api, args.staking_contract_address) if available_rewards == 0: # no rewards available, do nothing print("No rewards available. The service cannot be staked.") sys.exit(0) print(f"Rewards available: {available_rewards}. Staking the service...") - stake_txs = get_stake_txs(args.service_id, args.service_registry_address, args.staking_contract_address) + stake_txs = get_stake_txs( + ledger_api, + args.service_id, + args.service_registry_address, + args.staking_contract_address, + ) for tx in stake_txs: - send_tx_and_wait_for_receipt(owner_crypto, tx) + send_tx_and_wait_for_receipt(ledger_api, owner_crypto, tx) print(f"Service {args.service_id} staked successfully.") except Exception as e: # pylint: disable=broad-except diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 00000000..4f713f97 --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 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 package contains utils for working with the staking contract.""" + +import typing + +from aea.contracts.base import Contract +from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto +from eth_typing import HexStr + +from packages.valory.contracts.erc20.contract import ( + ERC20, +) +from packages.valory.contracts.service_staking_token.contract import ( + ServiceStakingTokenContract, +) + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +ZERO_ETH = 0 + +ContractType = typing.TypeVar("ContractType") + +GAS_PARAMS = { + "maxFeePerGas": 30_000_000_000, + "maxPriorityFeePerGas": 3_000_000_000, + "gas": 500_000, +} + + +def load_contract(ctype: ContractType) -> ContractType: + """Load contract.""" + *parts, _ = ctype.__module__.split(".") + path = "/".join(parts) + return Contract.from_dir(directory=path) + + +def get_approval_tx( + ledger_api: EthereumApi, + token: str, + spender: str, + amount: int, +) -> typing.Dict[str, typing.Any]: + """Get approval tx""" + approval_tx_data = erc20.build_approval_tx( + ledger_api, + token, + spender, + amount, + ).pop("data") + approval_tx = { + "data": approval_tx_data, + "to": token, + "value": ZERO_ETH, + } + return approval_tx + + +def get_balances( + ledger_api: EthereumApi, + token: str, + owner: str, +) -> typing.Tuple[int, int]: + """Returns the native and token balance of owner.""" + balances = erc20.check_balance(ledger_api, token, owner) + token_balance, native_balance = balances.pop("token"), balances.pop("wallet") + return token_balance, native_balance + + +def get_allowance( + ledger_api: EthereumApi, + token: str, + owner: str, + spender: str, +) -> int: + """Returns the allowance of owner for spender.""" + allowance = erc20.get_allowance(ledger_api, token, owner, spender).pop("data") + return allowance + + +def get_stake_txs( + ledger_api: EthereumApi, + service_id: int, + service_registry_address: str, + staking_contract_address: str, +) -> typing.List: + """Stake the service""" + # 1. approve the service to make use of the + + # we make use of the ERC20 contract to build the approval transaction + # since it has the same interface as ERC721 + # we use the ZERO_ADDRESS as the contract address since we don't do any contract interaction here, + # we are simply encoding + approval_tx = get_approval_tx( + erc20, service_registry_address, staking_contract_address, service_id + ) + + # 2. stake the service + stake_tx_data = staking_contract.build_stake_tx( + ledger_api, staking_contract_address, service_id + ).pop("data") + stake_tx = { + "data": stake_tx_data, + "to": staking_contract_address, + "value": ZERO_ETH, + } + return [approval_tx, stake_tx] + + +def get_unstake_txs( + ledger_api: EthereumApi, service_id: int, staking_contract_address: str +) -> typing.List: + """Get unstake txs""" + + unstake_tx_data = staking_contract.build_unstake_tx( + ledger_api, staking_contract_address, service_id + ).pop("data") + unstake_tx = { + "data": unstake_tx_data, + "to": staking_contract_address, + "value": ZERO_ETH, + } + + return [unstake_tx] + + +def get_available_rewards( + ledger_api: EthereumApi, staking_contract_address: str +) -> int: + """Get available rewards.""" + rewards = staking_contract.available_rewards( + ledger_api, staking_contract_address + ).pop("data") + return rewards + + +def is_service_staked( + ledger_api: EthereumApi, service_id: int, staking_contract_address: str +) -> bool: + """Check if service is staked.""" + is_staked = staking_contract.is_service_staked( + ledger_api, staking_contract_address, service_id + ).pop("data") + return is_staked + + +def get_next_checkpoint_ts( + ledger_api: EthereumApi, service_id: int, staking_contract_address: str +) -> int: + """Check if service is staked.""" + checkpoint_ts = staking_contract.get_next_checkpoint_ts( + ledger_api, staking_contract_address, service_id + ).pop("data") + return checkpoint_ts + + +def get_staking_rewards( + ledger_api: EthereumApi, service_id: int, staking_contract_address: str +) -> int: + """Check if service is staked.""" + rewards = staking_contract.get_staking_rewards( + ledger_api, staking_contract_address, service_id + ).pop("data") + return rewards + + +def send_tx( + ledger_api: EthereumApi, + crypto: EthereumCrypto, + raw_tx: typing.Dict[str, typing.Any], +) -> str: + """Send transaction.""" + raw_tx = { + **raw_tx, + **GAS_PARAMS, + "from": crypto.address, + "nonce": ledger_api.api.eth.get_transaction_count(crypto.address), + "chainId": ledger_api.api.eth.chain_id, + } + signed_tx = crypto.sign_transaction(raw_tx) + tx_digest = typing.cast( + str, ledger_api.send_signed_transaction(signed_tx, raise_on_try=True) + ) + return tx_digest + + +def send_tx_and_wait_for_receipt( + ledger_api: EthereumApi, + crypto: EthereumCrypto, + raw_tx: typing.Dict[str, typing.Any], +) -> typing.Dict[str, typing.Any]: + """Send transaction and wait for receipt.""" + tx_digest = HexStr(send_tx(ledger_api, crypto, raw_tx)) + receipt = ledger_api.api.eth.wait_for_transaction_receipt(tx_digest) + if receipt["status"] != 1: + raise ValueError("Transaction failed. Receipt:", receipt) + return receipt + + +staking_contract = typing.cast( + typing.Type[ServiceStakingTokenContract], load_contract(ServiceStakingTokenContract) +) +erc20 = typing.cast(typing.Type[ERC20], load_contract(ERC20)) From 1141788a18b709aa705446026a0874e277f2fc2e Mon Sep 17 00:00:00 2001 From: Ardian Date: Tue, 31 Oct 2023 21:01:26 +0100 Subject: [PATCH 07/10] feat: introduce `--with-staking` flag --- run_service.sh | 262 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 171 insertions(+), 91 deletions(-) diff --git a/run_service.sh b/run_service.sh index 3e971b77..f3768e1c 100755 --- a/run_service.sh +++ b/run_service.sh @@ -194,6 +194,29 @@ get_on_chain_service_state() { echo "$state" } +# check the balances of the safe and the operator +check_balances() { + minimum_olas_balance=1000000000000000000 + output=$(poetry run python "../scripts/approval.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "../$operator_pkey_path" "$CUSTOM_OLAS_ADDRESS" "$minimum_olas_balance" "$rpc") + if [[ $? -ne 0 ]]; then + echo "Checking ERC20 balance and approval failed.\n$output" + exit 1 + fi + echo "$output" +} + +# stake or unstake a service +perform_staking_ops() { + local unstake="$1" + output=$(poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" "$SKIP_LAST_EPOCH_REWARDS") + if [[ $? -ne 0 ]]; then + echo "Swapping Safe owner failed.\n$output" + exit 1 + fi + echo "$output" +} + + store=".trader_runner" rpc_path="$store/rpc.txt" operator_keys_file="$store/operator_keys.json" @@ -206,6 +229,15 @@ service_id_path="$store/service_id.txt" service_safe_address_path="$store/service_safe_address.txt" store_readme_path="$store/README.txt" +# Check the command-line arguments +while [[ "$#" -gt 0 ]]; do + case $1 in + --with-staking) use_staking=true ;; + *) echo "Unknown parameter: $1" ;; + esac + shift +done + # Function to create the .trader_runner storage create_storage() { local rpc="$1" @@ -402,11 +434,15 @@ 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="0x92499E80f50f06C4078794C179986907e7822Ea1" +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="0x3d77596beb0f130a4415df3D2D8232B3d3D31e44" export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" export AGENT_ID=12 +export MECH_AGENT_ADDRESS="0x77af31De935740567Cf4fF1986D04B2c964A786a" + if [ -z ${service_id+x} ]; then @@ -417,7 +453,7 @@ then echo "[Service owner] Minting your service on the Gnosis chain..." # create service - cost_of_bonding=10000000000000000 + cost_of_bonding=1000000000000000000 nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" service_id=$(poetry run autonomy mint \ --skip-hash-check \ @@ -428,6 +464,7 @@ then -a $AGENT_ID \ -n $n_agents \ --threshold $n_agents \ + --token $CUSTOM_OLAS_ADDRESS \ -c $cost_of_bonding ) # parse only the id from the response @@ -459,98 +496,122 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then echo "The operator and agent addresses need to have enough funds so that the process is not interrupted." echo "" - # Check balances - suggested_amount=50000000000000000 - ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" - - suggested_amount=50000000000000000 - ensure_minimum_balance $agent_address $suggested_amount "agent instance's address" - - echo "------------------------------" - echo "Updating on-chain service $service_id" - echo "------------------------------" - echo "" - echo "PLEASE, DO NOT INTERRUPT THIS PROCESS." - echo "" - echo "Cancelling the on-chain service update prematurely could lead to an inconsistent state of the Safe or the on-chain service state, which may require manual intervention to resolve." - echo "" - - # TODO this condition should be increased to be service_state=DEPLOYED && current_safe_owner=agent_address. - # Otherwise the script will not recover the on-chain state in the (rare) case where this transaction succeeds but terminating transaction fails. - if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then - # transfer the ownership of the Safe from the agent to the service owner - # (in a live service, this should be done by sending a 0 DAI transfer to its Safe) - service_safe_address=$(<"../$service_safe_address_path") - echo "[Agent instance] Swapping Safe owner..." - output=$(poetry run python "../scripts/swap_safe_owner.py" "$service_safe_address" "../$agent_pkey_path" "$operator_address" "$rpc") - if [[ $? -ne 0 ]]; then - echo "Swapping Safe owner failed.\n$output" - exit 1 - fi - echo "$output" - fi - - # terminate current service - if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then - echo "[Service owner] Terminating on-chain service $service_id..." - output=$( - poetry run autonomy service \ - --use-custom-chain \ - terminate "$service_id" \ - --key "../$operator_pkey_path" - ) - if [[ $? -ne 0 ]]; then - echo "Terminating service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi - fi - - # unbond current service - if [ "$(get_on_chain_service_state "$service_id")" == "TERMINATED_BONDED" ]; then - echo "[Operator] Unbonding on-chain service $service_id..." - output=$( - poetry run autonomy service \ - --use-custom-chain \ - unbond "$service_id" \ - --key "../$operator_pkey_path" - ) - if [[ $? -ne 0 ]]; then - echo "Unbonding service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi - fi - - # update service - if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then - echo "[Service owner] Updating on-chain service $service_id..." - cost_of_bonding=10000000000000000 - nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" - output=$( - poetry run autonomy mint \ - --skip-hash-check \ - --use-custom-chain \ - service packages/valory/services/trader/ \ - --key "../$operator_pkey_path" \ - --nft $nft \ - -a $AGENT_ID \ - -n $n_agents \ - --threshold $n_agents \ - -c $cost_of_bonding \ - --update "$service_id" - ) - if [[ $? -ne 0 ]]; then - echo "Updating service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi + response="n" + if [ "${use_staking}" = true ]; then + echo "Warning: updating the on-chain may require that your service is unstaked." + echo "Continuing will automatically unstake your service if it is staked, which may effect your staking rewards." + echo "Do you want to continue? [y/N]" + read -r response fi - echo "" - echo "Finished updating on-chain service $service_id." + if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then + echo "Skipping on-chain hash update." + else + # unstake the service + if [ "${use_staking}" = true ]; then + perform_staking_ops true + fi + + # Check balances + suggested_amount=50000000000000000 + ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" + + suggested_amount=50000000000000000 + ensure_minimum_balance $agent_address $suggested_amount "agent instance's address" + + echo "------------------------------" + echo "Updating on-chain service $service_id" + echo "------------------------------" + echo "" + echo "PLEASE, DO NOT INTERRUPT THIS PROCESS." + echo "" + echo "Cancelling the on-chain service update prematurely could lead to an inconsistent state of the Safe or the on-chain service state, which may require manual intervention to resolve." + echo "" + + # TODO this condition should be increased to be service_state=DEPLOYED && current_safe_owner=agent_address. + # Otherwise the script will not recover the on-chain state in the (rare) case where this transaction succeeds but terminating transaction fails. + if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then + # transfer the ownership of the Safe from the agent to the service owner + # (in a live service, this should be done by sending a 0 DAI transfer to its Safe) + service_safe_address=$(<"../$service_safe_address_path") + echo "[Agent instance] Swapping Safe owner..." + output=$(poetry run python "../scripts/swap_safe_owner.py" "$service_safe_address" "../$agent_pkey_path" "$operator_address" "$rpc") + if [[ $? -ne 0 ]]; then + echo "Swapping Safe owner failed.\n$output" + exit 1 + fi + echo "$output" + fi + + # terminate current service + if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then + echo "[Service owner] Terminating on-chain service $service_id..." + output=$( + poetry run autonomy service \ + --use-custom-chain \ + terminate "$service_id" \ + --key "../$operator_pkey_path" + ) + if [[ $? -ne 0 ]]; then + echo "Terminating service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi + fi + + # unbond current service + if [ "$(get_on_chain_service_state "$service_id")" == "TERMINATED_BONDED" ]; then + echo "[Operator] Unbonding on-chain service $service_id..." + output=$( + poetry run autonomy service \ + --use-custom-chain \ + unbond "$service_id" \ + --key "../$operator_pkey_path" + ) + if [[ $? -ne 0 ]]; then + echo "Unbonding service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi + fi + + # update service + if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then + echo "[Service owner] Updating on-chain service $service_id..." + nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" + cmd = "poetry run autonomy mint \ + --skip-hash-check \ + --use-custom-chain \ + service packages/valory/services/trader/ \ + --key \"../$operator_pkey_path\" \ + --nft $nft \ + -a $AGENT_ID \ + -n $n_agents \ + --threshold $n_agents \ + --update \"$service_id\" + " + if [ "${use_staking}" = true ]; then + cost_of_bonding=1000000000000000000 + cmd+=" -c $cost_of_bonding --token $CUSTOM_OLAS_ADDRESS" + else + cost_of_bonding=10000000000000000 + cmd+=" -c $cost_of_bonding" + fi + + output=$(eval "$cmd") + if [[ $? -ne 0 ]]; then + echo "Updating service failed.\n$output" + echo "Please, delete or rename the ./trader folder and try re-run this script again." + exit 1 + fi + fi + + echo "" + echo "Finished updating on-chain service $service_id." + fi fi + echo "" echo "Ensuring on-chain service $service_id is in DEPLOYED state..." @@ -562,7 +623,12 @@ fi # activate service if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then echo "[Service owner] Activating registration for on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" "$service_id") + cmd = "poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" "$service_id"" + if [ "${use_staking}" = true ]; then + check_balances + cmd+=" --token $CUSTOM_OLAS_ADDRESS" + fi + output=$(eval "$cmd") if [[ $? -ne 0 ]]; then echo "Activating service failed.\n$output" echo "Please, delete or rename the ./trader folder and try re-run this script again." @@ -573,7 +639,14 @@ fi # register agent instance if [ "$(get_on_chain_service_state "$service_id")" == "ACTIVE_REGISTRATION" ]; then echo "[Operator] Registering agent instance for on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address") + cmd = "poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address"" + + if [ "${use_staking}" = true ]; then + check_balances + cmd+=" --token $CUSTOM_OLAS_ADDRESS" + fi + + output=$(eval "$cmd") if [[ $? -ne 0 ]]; then echo "Registering agent instance failed.\n$output" echo "Please, delete or rename the ./trader folder and try re-run this script again." @@ -601,6 +674,13 @@ elif [ "$service_state" == "FINISHED_REGISTRATION" ]; then fi fi +# perform staking operations +# the following will stake the service in case it is not staked, and there are available rewards +# if the service is already staked, and there are no available rewards, it will unstake the service +if [ "${use_staking}" = true ]; then + perform_staking_ops +fi + # check state service_state="$(get_on_chain_service_state "$service_id")" if [ "$service_state" != "DEPLOYED" ]; then From a40aef6bd4e1437fd8a855722ac9a2eade3b73f0 Mon Sep 17 00:00:00 2001 From: Ardian Date: Tue, 31 Oct 2023 22:17:38 +0100 Subject: [PATCH 08/10] chore: cleanup run_service script --- run_service.sh | 89 ++- run_service_staking.sh | 752 ------------------ scripts/approval.py | 108 --- .../{wxdai_balance.py => erc20_balance.py} | 11 +- scripts/staking.py | 6 +- scripts/utils.py | 4 +- 6 files changed, 75 insertions(+), 895 deletions(-) delete mode 100755 run_service_staking.sh delete mode 100644 scripts/approval.py rename scripts/{wxdai_balance.py => erc20_balance.py} (84%) diff --git a/run_service.sh b/run_service.sh index f3768e1c..20026c0b 100755 --- a/run_service.sh +++ b/run_service.sh @@ -45,17 +45,17 @@ ensure_minimum_balance() { local address="$1" local minimum_balance="$2" local address_description="$3" - local wxdai="${4:-false}" + local token="${4:-"0x0000000000000000000000000000000000000000"}" - wxdai_balance=0 - if [ "$wxdai" = "true" ] + erc20_balance=0 + if [ ! "$token" = "0x0000000000000000000000000000000000000000" ] then - wxdai_balance=$(poetry run python "../scripts/wxdai_balance.py" "$safe" "$rpc") + erc20_balance=$(poetry run python "../scripts/erc20_balance.py" "$token" "$address" "$rpc") fi balance_hex=$(get_balance "$address") balance=$(hex_to_decimal "$balance_hex") - balance=$($PYTHON_CMD -c "print(int($balance) + int($wxdai_balance))") + balance=$($PYTHON_CMD -c "print(int($balance) + int($erc20_balance))") echo "Checking balance of $address_description (minimum required $(wei_to_dai "$minimum_balance") DAI):" echo " - Address: $address" @@ -78,7 +78,7 @@ ensure_minimum_balance() { if [ "$cycle_count" -eq 100 ]; then balance_hex=$(get_balance "$address") balance=$(hex_to_decimal "$balance_hex") - balance=$((wxdai_balance+balance)) + balance=$((erc20_balance+balance)) cycle_count=0 fi done @@ -92,6 +92,53 @@ ensure_minimum_balance() { echo "" } +# ensure erc20 balance +ensure_erc20_balance() { + local address="$1" + local minimum_balance="$2" + local address_description="$3" + local token="$4" + local token_name="$5" + + balance=0 + if [ ! "$token" = "0x0000000000000000000000000000000000000000" ] + then + balance=$(poetry run python "../scripts/erc20_balance.py" "$token" "$address" "$rpc") + fi + + echo "Checking balance of $address_description (minimum required $(wei_to_dai "$minimum_balance") $token_name):" + echo " - Address: $address" + echo " - Balance: $(wei_to_dai "$balance") $token_name" + + if [ "$($PYTHON_CMD -c "print($balance < $minimum_balance)")" == "True" ]; then + echo "" + echo " Please, fund address $address with at least $(wei_to_dai "$minimum_balance")." + + local spin='-\|/' + local i=0 + local cycle_count=0 + while [ "$($PYTHON_CMD -c "print($balance < $minimum_balance)")" == "True" ]; do + printf "\r Waiting... %s" "${spin:$i:1} " + i=$(((i + 1) % 4)) + sleep .1 + + # This will be checked every 10 seconds (100 cycles). + cycle_count=$((cycle_count + 1)) + if [ "$cycle_count" -eq 100 ]; then + balance=$(poetry run python "../scripts/erc20_balance.py" "$token" "$address" "$rpc") + cycle_count=0 + fi + done + + printf "\r Waiting... \n" + echo "" + echo " - Updated balance: $(wei_to_dai "$balance") $token_name" + fi + + echo " OK." + echo "" +} + # Get the address from a keys.json file get_address() { local keys_json_path="$1" @@ -194,17 +241,6 @@ get_on_chain_service_state() { echo "$state" } -# check the balances of the safe and the operator -check_balances() { - minimum_olas_balance=1000000000000000000 - output=$(poetry run python "../scripts/approval.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "../$operator_pkey_path" "$CUSTOM_OLAS_ADDRESS" "$minimum_olas_balance" "$rpc") - if [[ $? -ne 0 ]]; then - echo "Checking ERC20 balance and approval failed.\n$output" - exit 1 - fi - echo "$output" -} - # stake or unstake a service perform_staking_ops() { local unstake="$1" @@ -393,7 +429,7 @@ fi directory="trader" # This is a tested version that works well. # Feel free to replace this with a different version of the repo, but be careful as there might be breaking changes -service_version="v0.7.2" +service_version="feat/staking" service_repo=https://github.com/valory-xyz/$directory.git if [ -d $directory ] then @@ -438,10 +474,11 @@ export CUSTOM_STAKING_ADDRESS="0x92499E80f50f06C4078794C179986907e7822Ea1" 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="0x3d77596beb0f130a4415df3D2D8232B3d3D31e44" +export CUSTOM_GNOSIS_SAFE_SAME_ADDRESS_MULTISIG_ADDRESS="0x6e7f594f680f7aBad18b7a63de50F0FeE47dfD06" export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" export AGENT_ID=12 export MECH_AGENT_ADDRESS="0x77af31De935740567Cf4fF1986D04B2c964A786a" +export WXDAI_ADDRESS="0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" if [ -z ${service_id+x} ]; @@ -579,7 +616,7 @@ if [ "$local_service_hash" != "$remote_service_hash" ]; then if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then echo "[Service owner] Updating on-chain service $service_id..." nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" - cmd = "poetry run autonomy mint \ + export cmd="poetry run autonomy mint \ --skip-hash-check \ --use-custom-chain \ service packages/valory/services/trader/ \ @@ -623,9 +660,10 @@ fi # activate service if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then echo "[Service owner] Activating registration for on-chain service $service_id..." - cmd = "poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" "$service_id"" + export cmd="poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" "$service_id"" if [ "${use_staking}" = true ]; then - check_balances + minimum_olas_balance=1000000000000000000 + ensure_erc20_balance "$operator_address" $minimum_olas_balance "operator's address" $CUSTOM_OLAS_ADDRESS "OLAS" cmd+=" --token $CUSTOM_OLAS_ADDRESS" fi output=$(eval "$cmd") @@ -639,10 +677,11 @@ fi # register agent instance if [ "$(get_on_chain_service_state "$service_id")" == "ACTIVE_REGISTRATION" ]; then echo "[Operator] Registering agent instance for on-chain service $service_id..." - cmd = "poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address"" + export cmd="poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address"" if [ "${use_staking}" = true ]; then - check_balances + minimum_olas_balance=1000000000000000000 + ensure_erc20_balance "$operator_address" $minimum_olas_balance "operator's address" $CUSTOM_OLAS_ADDRESS "OLAS" cmd+=" --token $CUSTOM_OLAS_ADDRESS" fi @@ -741,7 +780,7 @@ suggested_amount=50000000000000000 ensure_minimum_balance "$agent_address" $suggested_amount "agent instance's address" suggested_amount=500000000000000000 -ensure_minimum_balance "$SAFE_CONTRACT_ADDRESS" $suggested_amount "service Safe's address" "true" +ensure_minimum_balance "$SAFE_CONTRACT_ADDRESS" $suggested_amount "service Safe's address" $WXDAI_ADDRESS if [ -d $directory ] then diff --git a/run_service_staking.sh b/run_service_staking.sh deleted file mode 100755 index ec9e69f3..00000000 --- a/run_service_staking.sh +++ /dev/null @@ -1,752 +0,0 @@ -#!/bin/bash - -# ------------------------------------------------------------------------------ -# -# Copyright 2023 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. -# -# ------------------------------------------------------------------------------ - -# Convert Hex to Dec -hex_to_decimal() { - $PYTHON_CMD -c "print(int('$1', 16))" -} - -# Convert Wei to Dai -wei_to_dai() { - local wei="$1" - local decimal_precision=4 # Change this to your desired precision - local dai=$($PYTHON_CMD -c "print('%.${decimal_precision}f' % ($wei / 1000000000000000000.0))") - echo "$dai" -} - -# Function to get the balance of an Ethereum address -get_balance() { - local address="$1" - curl -s -S -X POST \ - -H "Content-Type: application/json" \ - --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"$address\",\"latest\"],\"id\":1}" "$rpc" | \ - $PYTHON_CMD -c "import sys, json; print(json.load(sys.stdin)['result'])" -} - -# Function to ensure a minimum balance for an Ethereum address -ensure_minimum_balance() { - local address="$1" - local minimum_balance="$2" - local address_description="$3" - local wxdai="${4:-false}" - - wxdai_balance=0 - if [ "$wxdai" = "true" ] - then - wxdai_balance=$(poetry run python "../scripts/wxdai_balance.py" "$safe" "$rpc") - fi - - balance_hex=$(get_balance "$address") - balance=$(hex_to_decimal "$balance_hex") - balance=$($PYTHON_CMD -c "print(int($balance) + int($wxdai_balance))") - - echo "Checking balance of $address_description (minimum required $(wei_to_dai "$minimum_balance") DAI):" - echo " - Address: $address" - echo " - Balance: $(wei_to_dai "$balance") DAI" - - if [ "$($PYTHON_CMD -c "print($balance < $minimum_balance)")" == "True" ]; then - echo "" - echo " Please, fund address $address with at least $(wei_to_dai "$minimum_balance") DAI." - - local spin='-\|/' - local i=0 - local cycle_count=0 - while [ "$($PYTHON_CMD -c "print($balance < $minimum_balance)")" == "True" ]; do - printf "\r Waiting... %s" "${spin:$i:1} " - i=$(((i + 1) % 4)) - sleep .1 - - # This will be checked every 10 seconds (100 cycles). - cycle_count=$((cycle_count + 1)) - if [ "$cycle_count" -eq 100 ]; then - balance_hex=$(get_balance "$address") - balance=$(hex_to_decimal "$balance_hex") - balance=$((wxdai_balance+balance)) - cycle_count=0 - fi - done - - printf "\r Waiting... \n" - echo "" - echo " - Updated balance: $(wei_to_dai "$balance") DAI" - fi - - echo " OK." - echo "" -} - -# Get the address from a keys.json file -get_address() { - local keys_json_path="$1" - - if [ ! -f "$keys_json_path" ]; then - echo "Error: $keys_json_path does not exist." - return 1 - fi - - local address_start_position=17 - local address=$(sed -n 3p "$keys_json_path") - address=$(echo "$address" | - awk '{ print substr( $0, '$address_start_position', length($0) - '$address_start_position' - 1 ) }') - - echo -n "$address" -} - -# Get the private key from a keys.json file -get_private_key() { - local keys_json_path="$1" - - if [ ! -f "$keys_json_path" ]; then - echo "Error: $keys_json_path does not exist." - return 1 - fi - - local private_key_start_position=21 - local private_key=$(sed -n 4p "$keys_json_path") - private_key=$(echo -n "$private_key" | - awk '{ printf substr( $0, '$private_key_start_position', length($0) - '$private_key_start_position' ) }') - - private_key=$(echo -n "$private_key" | awk '{gsub(/\\"/, "\"", $0); print $0}') - private_key="${private_key#0x}" - - echo -n "$private_key" -} - -# Function to warm start the policy -warm_start() { - echo '["claude-prediction-offline", "claude-prediction-online", "deepmind-optimization", "deepmind-optimization-strong", "prediction-offline", "prediction-offline-sme", "prediction-online", "prediction-online-sme"]' >| sudo tee "$PWD/../$store/available_tools_store.json" - echo '{"counts": [23, 16, 54, 52, 20, 113, 33, 54], "eps": 0.1, "rewards": [0.21330030417382723, -0.06394516434480157, 0.5042296458897277, 0.38925697131774417, -0.2978133327512751, 1.055336834629253, -0.5935249657470777, 0.507192767958923]}' >| sudo tee "$PWD/../$store/policy_store.json" - echo '{}' >| sudo tee ".trader_runner/utilized_tools.json" -} - -# Function to add a volume to a service in a Docker Compose file -add_volume_to_service() { - local compose_file="$1" - local service_name="$2" - local volume_name="$3" - local volume_path="$4" - - # Check if the Docker Compose file exists - if [ ! -f "$compose_file" ]; then - echo "Docker Compose file '$compose_file' not found." - return 1 - fi - - # Check if the service exists in the Docker Compose file - if ! grep -q "^[[:space:]]*${service_name}:" "$compose_file"; then - echo "Service '$service_name' not found in '$compose_file'." - return 1 - fi - - if grep -q "^[[:space:]]*volumes:" "$compose_file"; then - awk -v volume_path="$volume_path" -v volume_name="$volume_name" ' - /^ *volumes:/ { - found_volumes = 1 - print - print " - " volume_path ":" volume_name ":Z" - next - } - 1 - ' "$compose_file" > temp_compose_file - else - awk -v service_name="$service_name" -v volume_path="$volume_path" -v volume_name="$volume_name" ' - /^ *'"$service_name"':/ { - found_service = 1 - print - print " volumes:" - print " - " volume_path ":" volume_name ":Z" - next - } - /^ *$/ && found_service == 1 { - print " volumes:" - print " - " volume_path ":" volume_name ":Z" - found_service = 0 - } - 1 - ' "$compose_file" > temp_compose_file - fi - - mv temp_compose_file "$compose_file" -} - -# Function to retrieve on-chain service state (requires env variables set to use --use-custom-chain) -get_on_chain_service_state() { - local service_id="$1" - local service_info=$(poetry run autonomy service --use-custom-chain info "$service_id") - local state="$(echo "$service_info" | awk '/Service State/ {sub(/\|[ \t]*Service State[ \t]*\|[ \t]*/, ""); sub(/[ \t]*\|[ \t]*/, ""); print}')" - echo "$state" -} - -# check the balances of the safe and the operator -check_balances() { - minimum_olas_balance=1000000000000000000 - output=$(poetry run python "../scripts/approval.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "../$operator_pkey_path" "$CUSTOM_OLAS_ADDRESS" "$minimum_olas_balance" "$rpc") - if [[ $? -ne 0 ]]; then - echo "Checking ERC20 balance and approval failed.\n$output" - exit 1 - fi - echo "$output" -} - -# stake or unstake a service -perform_staking_ops() { - local unstake="$1" - output=$(poetry run python "../scripts/staking.py" "$service_id" "$CUSTOM_SERVICE_REGISTRY_ADDRESS" "$CUSTOM_STAKING_ADDRESS" "../$operator_pkey_path" "$rpc" "$unstake" "$SKIP_LAST_EPOCH_REWARDS") - if [[ $? -ne 0 ]]; then - echo "Swapping Safe owner failed.\n$output" - exit 1 - fi - echo "$output" -} - - -store=".trader_runner" -rpc_path="$store/rpc.txt" -operator_keys_file="$store/operator_keys.json" -operator_pkey_path="$store/operator_pkey.txt" -keys_json="keys.json" -keys_json_path="$store/$keys_json" -agent_pkey_path="$store/agent_pkey.txt" -agent_address_path="$store/agent_address.txt" -service_id_path="$store/service_id.txt" -service_safe_address_path="$store/service_safe_address.txt" -store_readme_path="$store/README.txt" - -# Function to create the .trader_runner storage -create_storage() { - local rpc="$1" - - echo "This is the first run of the script. The script will generate new operator and agent instance addresses." - echo "" - - mkdir "../$store" - - # Generate README.txt file - echo -e 'IMPORTANT:\n\n' \ - ' This folder contains crucial configuration information and autogenerated keys for your Trader agent.\n' \ - ' Please back up this folder and be cautious if you are modifying or sharing these files to avoid potential asset loss.' > "../$store_readme_path" - - # Generate the RPC file - echo -n "$rpc" > "../$rpc_path" - - # Generate the operator's key - poetry run autonomy generate-key -n1 ethereum - mv "$keys_json" "../$operator_keys_file" - operator_address=$(get_address "../$operator_keys_file") - operator_pkey=$(get_private_key "../$operator_keys_file") - echo -n "$operator_pkey" > "../$operator_pkey_path" - echo "Your operator's autogenerated public address: $operator_address" - echo "(The same address will be used as the service owner.)" - - # Generate the agent's key - poetry run autonomy generate-key -n1 ethereum - mv "$keys_json" "../$keys_json_path" - agent_address=$(get_address "../$keys_json_path") - agent_pkey=$(get_private_key "../$keys_json_path") - echo -n "$agent_pkey" > "../$agent_pkey_path" - echo -n "$agent_address" > "../$agent_address_path" - echo "Your agent instance's autogenerated public address: $agent_address" - echo "" -} - -# Function to read and load the .trader_runner storage information if it exists. -# Also sets `first_run` flag to identify whether we are running the script for the first time. -try_read_storage() { - if [ -d $store ]; then - - # INFO: This is a fix to avoid corrupting already-created stores - if [[ -f "$operator_keys_file" && ! -f "$operator_pkey_path" ]]; then - operator_pkey=$(get_private_key "$operator_keys_file") - echo -n "$operator_pkey" > "$operator_pkey_path" - fi - - # INFO: This is a fix to avoid corrupting already-created stores - if [[ -f "$keys_json_path" && ! -f "$agent_pkey_path" ]]; then - agent_pkey=$(get_private_key "$keys_json_path") - echo -n "$agent_pkey" > "$agent_pkey_path" - fi - - first_run=false - paths="$rpc_path $operator_keys_file $operator_pkey_path $keys_json_path $agent_address_path $agent_pkey_path $service_id_path" - - for file in $paths; do - if ! [ -f "$file" ]; then - if [ "$file" != $service_safe_address_path ] && [ "$file" != $service_id_path ]; then - echo "The runner's store is corrupted!" - echo "Please manually investigate the $store folder" - echo "Make sure that you do not lose your keys or any other important information!" - exit 1 - fi - fi - done - - rpc=$(cat $rpc_path) - agent_address=$(cat $agent_address_path) - operator_address=$(get_address "$operator_keys_file") - if [ -f "$service_id_path" ]; then - service_id=$(cat $service_id_path) - fi - else - first_run=true - fi -} - -# ------------------ -# Script starts here -# ------------------ - -set -e # Exit script on first error -echo "" -echo "---------------" -echo " Trader runner " -echo "---------------" -echo "" -echo "This script will assist you in setting up and running the Trader service (https://github.com/valory-xyz/trader)." -echo "" - -# Check if user is inside a venv -if [[ "$VIRTUAL_ENV" != "" ]] -then - echo "Please exit the virtual environment!" - exit 1 -fi - -# Check dependencies -if command -v python3 >/dev/null 2>&1; then - PYTHON_CMD="python3" -elif command -v python >/dev/null 2>&1; then - PYTHON_CMD="python" -else - echo >&2 "Python is not installed!"; - exit 1 -fi - -if [[ "$($PYTHON_CMD --version 2>&1)" != "Python 3.10."* ]] && [[ "$($PYTHON_CMD --version 2>&1)" != "Python 3.11."* ]]; then - echo >&2 "Python version >=3.10.0, <3.12.0 is required but found $($PYTHON_CMD --version 2>&1)"; - exit 1 -fi - -command -v git >/dev/null 2>&1 || -{ echo >&2 "Git is not installed!"; - exit 1 -} - -command -v poetry >/dev/null 2>&1 || -{ echo >&2 "Poetry is not installed!"; - exit 1 -} - -command -v docker >/dev/null 2>&1 || -{ echo >&2 "Docker is not installed!"; - exit 1 -} - -docker rm -f abci0 node0 trader_abci_0 trader_tm_0 &> /dev/null || -{ echo >&2 "Docker is not running!"; - exit 1 -} - -try_read_storage - -# Prompt for RPC -[[ -z "${rpc}" ]] && read -rsp "Enter a Gnosis RPC that supports eth_newFilter [hidden input]: " rpc && echo || rpc="${rpc}" - -# Check if eth_newFilter is supported -new_filter_supported=$(curl -s -S -X POST \ - -H "Content-Type: application/json" \ - --data '{"jsonrpc":"2.0","method":"eth_newFilter","params":["invalid"],"id":1}' "$rpc" | \ - $PYTHON_CMD -c "import sys, json; print(json.load(sys.stdin)['error']['message']=='The method eth_newFilter does not exist/is not available')") - -if [ "$new_filter_supported" = True ] -then - echo "The given RPC ($rpc) does not support 'eth_newFilter'! Terminating script..." - exit 1 -fi - -# clone repo -directory="trader" -# This is a tested version that works well. -# Feel free to replace this with a different version of the repo, but be careful as there might be breaking changes -service_version="feat/staking" -service_repo=https://github.com/valory-xyz/$directory.git -if [ -d $directory ] -then - echo "Detected an existing $directory repo. Using this one..." - echo "Please stop and manually delete the $directory repo if you updated the service's version ($service_version)!" - echo "You can run the following command, or continue with the pre-existing version of the service:" - echo "rm -r $directory" -else - echo "Cloning the $directory repo..." - git clone --depth 1 --branch $service_version $service_repo -fi - -cd $directory -if [ "$(git rev-parse --is-inside-work-tree)" = true ] -then - poetry install - poetry run autonomy packages sync -else - echo "$directory is not a git repo!" - exit 1 -fi - -if [ "$first_run" = "true" ] -then - create_storage "$rpc" -fi - -echo "" -echo "-----------------------------------------" -echo "Checking Autonolas Protocol service state" -echo "-----------------------------------------" - -gnosis_chain_id=100 -n_agents=1 - -# setup the minting tool -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="0x92499E80f50f06C4078794C179986907e7822Ea1" -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="0x3d77596beb0f130a4415df3D2D8232B3d3D31e44" -export CUSTOM_MULTISEND_ADDRESS="0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" -export AGENT_ID=12 -export MECH_AGENT_ADDRESS="0x77af31De935740567Cf4fF1986D04B2c964A786a" - - -if [ -z ${service_id+x} ]; -then - # Check balances - suggested_amount=50000000000000000 - ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" - - echo "[Service owner] Minting your service on the Gnosis chain..." - - # create service - cost_of_bonding=1000000000000000000 - nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" - service_id=$(poetry run autonomy mint \ - --skip-hash-check \ - --use-custom-chain \ - service packages/valory/services/$directory/ \ - --key "../$operator_pkey_path" \ - --nft $nft \ - -a $AGENT_ID \ - -n $n_agents \ - --threshold $n_agents \ - --token $CUSTOM_OLAS_ADDRESS \ - -c $cost_of_bonding - ) - # parse only the id from the response - service_id="${service_id##*: }" - # validate id - if ! [[ "$service_id" =~ ^[0-9]+$ || "$service_id" =~ ^[-][0-9]+$ ]] - then - echo "Service minting failed: $service_id" - exit 1 - fi - - echo -n "$service_id" > "../$service_id_path" -fi - -# Update the on-chain service if outdated -packages="packages/packages.json" -local_service_hash="$(grep 'service' $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") - -if [ "$local_service_hash" != "$remote_service_hash" ]; then - echo "" - echo "Your currently minted on-chain service (id $service_id) mismatches the fetched trader service ($service_version):" - echo " - Local service hash ($service_version): $local_service_hash" - echo " - On-chain service hash (id $service_id): $remote_service_hash" - echo "" - echo "This is most likely caused due to an update of the trader service code." - echo "The script will proceed now to update the on-chain service." - echo "The operator and agent addresses need to have enough funds so that the process is not interrupted." - echo "" - - echo "Warning: updating the on-chain may require that your service is unstaked." - echo "Continuing will automatically unstake your service if it is staked, which may effect your staking rewards." - echo "Do you want to continue? [y/N]" - read -r response - - if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then - echo "Skipping on-chain hash update." - else - # unstake the service - perform_staking_ops true - - # Check balances - suggested_amount=50000000000000000 - ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" - - suggested_amount=50000000000000000 - ensure_minimum_balance $agent_address $suggested_amount "agent instance's address" - - echo "------------------------------" - echo "Updating on-chain service $service_id" - echo "------------------------------" - echo "" - echo "PLEASE, DO NOT INTERRUPT THIS PROCESS." - echo "" - echo "Cancelling the on-chain service update prematurely could lead to an inconsistent state of the Safe or the on-chain service state, which may require manual intervention to resolve." - echo "" - - # TODO this condition should be increased to be service_state=DEPLOYED && current_safe_owner=agent_address. - # Otherwise the script will not recover the on-chain state in the (rare) case where this transaction succeeds but terminating transaction fails. - if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then - # transfer the ownership of the Safe from the agent to the service owner - # (in a live service, this should be done by sending a 0 DAI transfer to its Safe) - service_safe_address=$(<"../$service_safe_address_path") - echo "[Agent instance] Swapping Safe owner..." - output=$(poetry run python "../scripts/swap_safe_owner.py" "$service_safe_address" "../$agent_pkey_path" "$operator_address" "$rpc") - if [[ $? -ne 0 ]]; then - echo "Swapping Safe owner failed.\n$output" - exit 1 - fi - echo "$output" - fi - - # terminate current service - if [ "$(get_on_chain_service_state "$service_id")" == "DEPLOYED" ]; then - echo "[Service owner] Terminating on-chain service $service_id..." - output=$( - poetry run autonomy service \ - --use-custom-chain \ - terminate "$service_id" \ - --key "../$operator_pkey_path" - ) - if [[ $? -ne 0 ]]; then - echo "Terminating service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi - fi - - # unbond current service - if [ "$(get_on_chain_service_state "$service_id")" == "TERMINATED_BONDED" ]; then - echo "[Operator] Unbonding on-chain service $service_id..." - output=$( - poetry run autonomy service \ - --use-custom-chain \ - unbond "$service_id" \ - --key "../$operator_pkey_path" - ) - if [[ $? -ne 0 ]]; then - echo "Unbonding service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi - fi - - # update service - if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then - echo "[Service owner] Updating on-chain service $service_id..." - cost_of_bonding=1000000000000000000 - nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" - output=$( - poetry run autonomy mint \ - --skip-hash-check \ - --use-custom-chain \ - service packages/valory/services/trader/ \ - --key "../$operator_pkey_path" \ - --nft $nft \ - -a $AGENT_ID \ - -n $n_agents \ - --threshold $n_agents \ - --token $CUSTOM_OLAS_ADDRESS \ - -c $cost_of_bonding \ - --update "$service_id" - ) - if [[ $? -ne 0 ]]; then - echo "Updating service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi - fi - - echo "" - echo "Finished updating on-chain service $service_id." - fi -fi - - -echo "" -echo "Ensuring on-chain service $service_id is in DEPLOYED state..." - -if [ "$(get_on_chain_service_state "$service_id")" != "DEPLOYED" ]; then - suggested_amount=25000000000000000 - ensure_minimum_balance "$operator_address" $suggested_amount "operator's address" -fi - -# activate service -if [ "$(get_on_chain_service_state "$service_id")" == "PRE_REGISTRATION" ]; then - check_balances - echo "[Service owner] Activating registration for on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain activate --key "../$operator_pkey_path" "$service_id" --token $CUSTOM_OLAS_ADDRESS) - if [[ $? -ne 0 ]]; then - echo "Activating service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi -fi - -# register agent instance -if [ "$(get_on_chain_service_state "$service_id")" == "ACTIVE_REGISTRATION" ]; then - check_balances - echo "[Operator] Registering agent instance for on-chain service $service_id..." - cost_of_bonding=1000000000000000000 - output=$(poetry run autonomy service --use-custom-chain register --key "../$operator_pkey_path" "$service_id" -a $AGENT_ID -i "$agent_address" --token $CUSTOM_OLAS_ADDRESS) - if [[ $? -ne 0 ]]; then - echo "Registering agent instance failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi -fi - -# deploy on-chain service -service_state="$(get_on_chain_service_state "$service_id")" -if [ "$service_state" == "FINISHED_REGISTRATION" ] && [ "$first_run" = "true" ]; then - echo "[Service owner] Deploying on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path") - if [[ $? -ne 0 ]]; then - echo "Deploying service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi -elif [ "$service_state" == "FINISHED_REGISTRATION" ]; then - echo "[Service owner] Deploying on-chain service $service_id..." - output=$(poetry run autonomy service --use-custom-chain deploy "$service_id" --key "../$operator_pkey_path") - if [[ $? -ne 0 ]]; then - echo "Deploying service failed.\n$output" - echo "Please, delete or rename the ./trader folder and try re-run this script again." - exit 1 - fi -fi - -# perform staking operations -# the following will stake the service in case it is not staked, and there are available rewards -# if the service is already staked, and there are no available rewards, it will unstake the service -perform_staking_ops - -# check state -service_state="$(get_on_chain_service_state "$service_id")" -if [ "$service_state" != "DEPLOYED" ]; then - echo "Something went wrong while deploying on-chain service. The service's state is $service_state." - echo "Please check the output of the script and the on-chain registry for more information." - exit 1 -fi - -echo "" -echo "Finished checking Autonolas Protocol service $service_id state." - - -echo "" -echo "------------------------------" -echo "Starting the trader service..." -echo "------------------------------" -echo "" - -# Get the deployed service's Safe address from the contract -service_info=$(poetry run autonomy service --use-custom-chain info "$service_id") -safe=$(echo "$service_info" | grep "Multisig Address") -address_start_position=31 -safe=$(echo "$safe" | - awk '{ print substr( $0, '$address_start_position', length($0) - '$address_start_position' - 3 ) }') -export SAFE_CONTRACT_ADDRESS=$safe -echo -n "$safe" > "../$service_safe_address_path" - -echo "Your agent instance's address: $agent_address" -echo "Your service's Safe address: $safe" -echo "" - -# Set environment variables. Tweak these to modify your strategy -export RPC_0="$rpc" -export CHAIN_ID=$gnosis_chain_id -export ALL_PARTICIPANTS='["'$agent_address'"]' -# This is the default market creator. Feel free to update with other market creators -export OMEN_CREATORS='["0x89c5cc945dd550BcFfb72Fe42BfF002429F46Fec"]' -export BET_AMOUNT_PER_THRESHOLD_000=0 -export BET_AMOUNT_PER_THRESHOLD_010=0 -export BET_AMOUNT_PER_THRESHOLD_020=0 -export BET_AMOUNT_PER_THRESHOLD_030=0 -export BET_AMOUNT_PER_THRESHOLD_040=0 -export BET_AMOUNT_PER_THRESHOLD_050=0 -export BET_AMOUNT_PER_THRESHOLD_060=0 -export BET_AMOUNT_PER_THRESHOLD_070=0 -export BET_AMOUNT_PER_THRESHOLD_080=30000000000000000 -export BET_AMOUNT_PER_THRESHOLD_090=80000000000000000 -export BET_AMOUNT_PER_THRESHOLD_100=100000000000000000 -export BET_THRESHOLD=5000000000000000 -export PROMPT_TEMPLATE="Please take over the role of a Data Scientist to evaluate the given question. With the given question \"@{question}\" and the \`yes\` option represented by \`@{yes}\` and the \`no\` option represented by \`@{no}\`, what are the respective probabilities of \`p_yes\` and \`p_no\` occurring?" -export REDEEM_MARGIN_DAYS=24 - -service_dir="trader_service" -build_dir="abci_build" -directory="$service_dir/$build_dir" - -suggested_amount=50000000000000000 -ensure_minimum_balance "$agent_address" $suggested_amount "agent instance's address" - -suggested_amount=500000000000000000 -ensure_minimum_balance "$SAFE_CONTRACT_ADDRESS" $suggested_amount "service Safe's address" "true" - -if [ -d $directory ] -then - echo "Detected an existing build. Using this one..." - cd $service_dir - - if rm -rf "$build_dir"; then - echo "Directory "$build_dir" removed successfully." - else - # If the above command fails, use sudo to remove - echo "You will need to provide sudo password in order for the script to delete part of the build artifacts." - sudo rm -rf "$build_dir" - echo "Directory "$build_dir" removed successfully." - fi -else - echo "Setting up the service..." - - if ! [ -d "$service_dir" ]; then - # Fetch the service - poetry run autonomy fetch --local --service valory/trader --alias $service_dir - fi - - cd $service_dir - # Build the image - poetry run autonomy build-image - cp ../../$keys_json_path $keys_json -fi - -# Build the deployment with a single agent -poetry run autonomy deploy build --n $n_agents -ltm - -cd .. - -warm_start -add_volume_to_service "$PWD/trader_service/abci_build/docker-compose.yaml" "trader_abci_0" "/data" "$PWD/../$store/" - -# Run the deployment -poetry run autonomy deploy run --build-dir $directory --detach diff --git a/scripts/approval.py b/scripts/approval.py deleted file mode 100644 index ccedceec..00000000 --- a/scripts/approval.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2022-2023 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 script swaps ownership of a Safe with a single owner.""" - -import argparse -import sys -import traceback -from pathlib import Path - -from aea_ledger_ethereum.ethereum import EthereumApi, EthereumCrypto - -from utils import ( - get_balances, - send_tx_and_wait_for_receipt, - get_allowance, - get_approval_tx, -) - -if __name__ == "__main__": - try: - print(f" - Starting {Path(__file__).name} script...") - - parser = argparse.ArgumentParser( - description="Swap ownership of a Safe with a single owner on the Gnosis chain." - ) - parser.add_argument( - "service_id", - type=int, - help="The on-chain service id.", - ) - parser.add_argument( - "service_registry_address", - type=str, - help="The service registry contract address.", - ) - parser.add_argument( - "operator_private_key_path", - type=str, - help="Path to the file containing the service operator's Ethereum private key", - ) - parser.add_argument( - "olas_address", - type=str, - help="The address of the OLAS token.", - ) - parser.add_argument( - "minimum_olas_balance", - type=int, - help="The minimum OLAS balance required for agent registration.", - ) - parser.add_argument("rpc", type=str, help="RPC for the Gnosis chain") - args = parser.parse_args() - - ledger_api = EthereumApi(address=args.rpc) - owner_crypto = EthereumCrypto(private_key_path=args.operator_private_key_path) - token_balance, native_balance = get_balances( - ledger_api, args.olas_address, owner_crypto.address - ) - if token_balance < args.minimum_olas_balance: - raise ValueError( - f"Operator has insufficient OLAS balance. Required: {args.minimum_olas_balance}, Actual: {token_balance}" - ) - - if native_balance == 0: - raise ValueError("Operator has no xDAI.") - - allowance = get_allowance( - ledger_api, - args.olas_address, - owner_crypto.address, - args.service_registry_address, - ) - if allowance >= args.minimum_olas_balance: - print("Operator has sufficient OLAS allowance.") - sys.exit(0) - - approval_tx = get_approval_tx( - ledger_api, - args.olas_address, - args.service_registry_address, - args.minimum_olas_balance, - ) - send_tx_and_wait_for_receipt(ledger_api, owner_crypto, approval_tx) - print("Approved service registry to spend OLAS.") - sys.exit(0) - - except Exception as e: # pylint: disable=broad-except - print(f"An error occurred while executing {Path(__file__).name}: {str(e)}") - traceback.print_exc() - sys.exit(1) diff --git a/scripts/wxdai_balance.py b/scripts/erc20_balance.py similarity index 84% rename from scripts/wxdai_balance.py rename to scripts/erc20_balance.py index 16239d34..6d797737 100644 --- a/scripts/wxdai_balance.py +++ b/scripts/erc20_balance.py @@ -31,8 +31,8 @@ def get_balance() -> int: """Get the wxDAI balance of an address in WEI.""" w3 = Web3(HTTPProvider(rpc)) - contract_instance = w3.eth.contract(address=WXDAI_CONTRACT_ADDRESS, abi=abi) - return contract_instance.functions.balanceOf(address).call() + contract_instance = w3.eth.contract(address=token, abi=abi) + return contract_instance.functions.balanceOf(w3.to_checksum_address(address)).call() def read_abi() -> str: @@ -42,10 +42,11 @@ def read_abi() -> str: if __name__ == "__main__": - if len(sys.argv) != 3: + if len(sys.argv) != 4: raise ValueError("Expected the address and the rpc as positional arguments.") else: - address = sys.argv[1] - rpc = sys.argv[2] + token = sys.argv[1] + address = sys.argv[2] + rpc = sys.argv[3] abi = read_abi() print(get_balance()) diff --git a/scripts/staking.py b/scripts/staking.py index 829e690b..34a64dd6 100644 --- a/scripts/staking.py +++ b/scripts/staking.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ # -# Copyright 2022-2023 Valory AG +# Copyright 2023 Valory AG # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ # # ------------------------------------------------------------------------------ -"""This script swaps ownership of a Safe with a single owner.""" +"""This script performs staking related operations.""" import argparse import sys @@ -82,7 +82,7 @@ sys.exit(0) next_ts = get_next_checkpoint_ts( - ledger_api, args.service_id, args.staking_contract_address + ledger_api, args.staking_contract_address ) if next_ts > time.time() and not args.skip_livenesss_check: print( diff --git a/scripts/utils.py b/scripts/utils.py index 4f713f97..de5b8521 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -161,11 +161,11 @@ def is_service_staked( def get_next_checkpoint_ts( - ledger_api: EthereumApi, service_id: int, staking_contract_address: str + ledger_api: EthereumApi, staking_contract_address: str ) -> int: """Check if service is staked.""" checkpoint_ts = staking_contract.get_next_checkpoint_ts( - ledger_api, staking_contract_address, service_id + ledger_api, staking_contract_address ).pop("data") return checkpoint_ts From 4771afee33cb7cf86230b831d7aa72491845f342 Mon Sep 17 00:00:00 2001 From: Ardian Date: Wed, 1 Nov 2023 12:34:20 +0100 Subject: [PATCH 09/10] fix: approval of erc721 --- scripts/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils.py b/scripts/utils.py index de5b8521..b00814a8 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -108,7 +108,7 @@ def get_stake_txs( # we use the ZERO_ADDRESS as the contract address since we don't do any contract interaction here, # we are simply encoding approval_tx = get_approval_tx( - erc20, service_registry_address, staking_contract_address, service_id + ledger_api, service_registry_address, staking_contract_address, service_id ) # 2. stake the service From b37f720feee973d13e226f9e4d856ca4335b306e Mon Sep 17 00:00:00 2001 From: Ardian Date: Wed, 1 Nov 2023 12:45:17 +0100 Subject: [PATCH 10/10] fix: use `--token` only when running with staking --- run_service.sh | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/run_service.sh b/run_service.sh index 117cbfe5..ae2ef08a 100755 --- a/run_service.sh +++ b/run_service.sh @@ -470,8 +470,8 @@ 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="0x92499E80f50f06C4078794C179986907e7822Ea1" -export CUSTOM_OLAS_ADDRESS="0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f" +export CUSTOM_STAKING_ADDRESS="0x337b53b4471775a7F87c8D5d077B17BbF9a0D369" +export CUSTOM_OLAS_ADDRESS="0xFC846A9EfC5C00d347Fd5A3759017DebeAdA3B74" 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" @@ -490,20 +490,25 @@ then echo "[Service owner] Minting your service on the Gnosis chain..." # create service - cost_of_bonding=1000000000000000000 - nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" - service_id=$(poetry run autonomy mint \ + cmd="poetry run autonomy mint \ --skip-hash-check \ --use-custom-chain \ service packages/valory/services/$directory/ \ - --key "../$operator_pkey_path" \ + --key \"../$operator_pkey_path\" \ --nft $nft \ -a $AGENT_ID \ -n $n_agents \ - --threshold $n_agents \ - --token $CUSTOM_OLAS_ADDRESS \ - -c $cost_of_bonding - ) + --threshold $n_agents" + + if [ "${use_staking}" = true ]; then + cost_of_bonding=1000000000000000000 + cmd+=" -c $cost_of_bonding --token $CUSTOM_OLAS_ADDRESS" + else + cost_of_bonding=10000000000000000 + cmd+=" -c $cost_of_bonding" + fi + nft="bafybeig64atqaladigoc3ds4arltdu63wkdrk3gesjfvnfdmz35amv7faq" + service_id=$(eval $cmd) # parse only the id from the response service_id="${service_id##*: }" # validate id