From 5e3e87f9641cc5150fde6539f5c2277603756773 Mon Sep 17 00:00:00 2001 From: 0xDEFSER Date: Mon, 25 Nov 2024 17:51:10 +0100 Subject: [PATCH 1/6] Feature: InjectiveStaking add new compound_rewards function --- injective_functions/function_schemas.json | 14 ++++ injective_functions/functions_schemas.json | 14 ++++ injective_functions/staking/__init__.py | 83 ++++++++++++++++++- .../staking/staking_schema.json | 14 ++++ injective_functions/utils/function_helper.py | 1 + 5 files changed, 125 insertions(+), 1 deletion(-) diff --git a/injective_functions/function_schemas.json b/injective_functions/function_schemas.json index 81b8721..ff8530f 100644 --- a/injective_functions/function_schemas.json +++ b/injective_functions/function_schemas.json @@ -102,6 +102,20 @@ "required": ["validator_address", "amount"] } }, + { + "name": "compound_rewards", + "description": "Automatically reinvest your staking rewards with a specific validator to increase your staked amount.", + "parameters": { + "type": "object", + "properties": { + "validator_address": { + "type": "string", + "description": "Validator address you want to compound your rewards with." + } + }, + "required": ["validator_address"] + } + }, { "name": "transfer_funds", "description": "Transfer funds to another Injective address", diff --git a/injective_functions/functions_schemas.json b/injective_functions/functions_schemas.json index 891eabf..0ac10de 100644 --- a/injective_functions/functions_schemas.json +++ b/injective_functions/functions_schemas.json @@ -778,6 +778,20 @@ ] } }, + { + "name": "compound_rewards", + "description": "Automatically reinvest your staking rewards with a specific validator to increase your staked amount.", + "parameters": { + "type": "object", + "properties": { + "validator_address": { + "type": "string", + "description": "Validator address you want to compound your rewards with." + } + }, + "required": ["validator_address"] + } + }, { "name": "create_denom", "description": "Create a new token denomination", diff --git a/injective_functions/staking/__init__.py b/injective_functions/staking/__init__.py index 78ebe04..c3679ce 100644 --- a/injective_functions/staking/__init__.py +++ b/injective_functions/staking/__init__.py @@ -1,6 +1,7 @@ +import asyncio from decimal import Decimal from injective_functions.base import InjectiveBase -from typing import Dict, List +from typing import Dict """This class handles all account transfer within the account""" @@ -19,3 +20,83 @@ async def stake_tokens(self, validator_address: str, amount: str) -> Dict: amount=float(amount), ) return await self.chain_client.build_and_broadcast_tx(msg) + + async def compound_rewards(self, validator_address: str) -> Dict: + """ + Compounds staking rewards by withdrawing them and restaking. + :param validator_address: The validator's address + :return: Transaction result + """ + try: + # Step 1: Fetch the initial INJ balance + balance_response = await self.chain_client.client.get_bank_balance( + address=self.chain_client.address.to_acc_bech32(), + denom="inj" + ) + initial_balance = Decimal(balance_response.balance.amount) + + # Step 2: Withdraw rewards + withdraw_msg = self.chain_client.composer.msg_withdraw_delegator_reward( + delegator_address=self.chain_client.address.to_acc_bech32(), + validator_address=validator_address, + ) + withdraw_response = await self.chain_client.build_and_broadcast_tx(withdraw_msg) + + # Step 3: Wait for the balance to update + updated_balance = await self.wait_for_balance_update(old_balance=initial_balance, denom="inj") + + # Step 4: Calculate the withdrawn rewards + rewards_to_stake = updated_balance - initial_balance + if rewards_to_stake <= 0: + if rewards_to_stake < 0: + # Specific error for negative rewards + return { + "success": False, + "error": f"Rewards ({rewards_to_stake}) are lower than gas fees, resulting in a negative net " + f"reward." + } + # Generic error for zero rewards + return {"success": False, "error": "No rewards available to compound."} + + # Step 5: Restake the rewards + delegate_msg = self.chain_client.composer.MsgDelegate( + delegator_address=self.chain_client.address.to_acc_bech32(), + validator_address=validator_address, + amount=float(rewards_to_stake) / (10 ** 18), + ) + delegate_response = await self.chain_client.build_and_broadcast_tx(delegate_msg) + + return { + "success": True, + "withdraw_response": withdraw_response, + "delegate_response": delegate_response, + } + + except Exception as e: + return {"success": False, "error": str(e)} + + async def wait_for_balance_update( + self, + old_balance: Decimal, + denom: str, + timeout: int = 10, + interval: int = 1 + ) -> Decimal: + """ + Waits for the balance to update after a transaction. + :param old_balance: Previous balance to compare against + :param denom: Denomination of the token (e.g., "inj") + :param timeout: Total time to wait (in seconds) + :param interval: Time between balance checks (in seconds) + :return: Updated balance + """ + for _ in range(timeout // interval): + balance_response = await self.chain_client.client.get_bank_balance( + address=self.chain_client.address.to_acc_bech32(), + denom=denom + ) + updated_balance = Decimal(balance_response.balance.amount) + if updated_balance != old_balance: + return updated_balance + await asyncio.sleep(interval) + raise TimeoutError("Balance did not update within the timeout period.") diff --git a/injective_functions/staking/staking_schema.json b/injective_functions/staking/staking_schema.json index 33bfdef..4d3451c 100644 --- a/injective_functions/staking/staking_schema.json +++ b/injective_functions/staking/staking_schema.json @@ -17,6 +17,20 @@ }, "required": ["validator_address", "amount"] } + }, + { + "name": "compound_rewards", + "description": "Automatically reinvest your staking rewards with a specific validator to increase your staked amount.", + "parameters": { + "type": "object", + "properties": { + "validator_address": { + "type": "string", + "description": "Validator address you want to compound your rewards with." + } + }, + "required": ["validator_address"] + } } ] } \ No newline at end of file diff --git a/injective_functions/utils/function_helper.py b/injective_functions/utils/function_helper.py index 74d947c..261ea5d 100644 --- a/injective_functions/utils/function_helper.py +++ b/injective_functions/utils/function_helper.py @@ -45,6 +45,7 @@ class InjectiveFunctionMapper: "query_total_supply": ("bank", "query_total_supply"), # Staking functions "stake_tokens": ("staking", "stake_tokens"), + "compound_rewards": ("staking", "compound_rewards"), # Auction functions "send_bid_auction": ("auction", "send_bid_auction"), # Authz functions From 284c03b91c0678e262e0d52924b5f9a640b80685 Mon Sep 17 00:00:00 2001 From: 0xDEFSER Date: Mon, 25 Nov 2024 18:09:43 +0100 Subject: [PATCH 2/6] Bugfix: coderabbitai comments: - Validate interval to prevent division by zero - Catch specific exceptions instead of general Exception - Maintain precision by avoiding conversion to float --- injective_functions/staking/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/injective_functions/staking/__init__.py b/injective_functions/staking/__init__.py index c3679ce..ed19172 100644 --- a/injective_functions/staking/__init__.py +++ b/injective_functions/staking/__init__.py @@ -62,7 +62,7 @@ async def compound_rewards(self, validator_address: str) -> Dict: delegate_msg = self.chain_client.composer.MsgDelegate( delegator_address=self.chain_client.address.to_acc_bech32(), validator_address=validator_address, - amount=float(rewards_to_stake) / (10 ** 18), + amount=rewards_to_stake / (10 ** 18), ) delegate_response = await self.chain_client.build_and_broadcast_tx(delegate_msg) @@ -72,7 +72,7 @@ async def compound_rewards(self, validator_address: str) -> Dict: "delegate_response": delegate_response, } - except Exception as e: + except (TimeoutError, ValueError) as e: return {"success": False, "error": str(e)} async def wait_for_balance_update( @@ -90,6 +90,9 @@ async def wait_for_balance_update( :param interval: Time between balance checks (in seconds) :return: Updated balance """ + if interval <= 0: + raise ValueError("Interval must be greater than zero.") + for _ in range(timeout // interval): balance_response = await self.chain_client.client.get_bank_balance( address=self.chain_client.address.to_acc_bech32(), From 386e466d7c910fcd77ad7748094f13ed79dd62b1 Mon Sep 17 00:00:00 2001 From: 0xDEFSER Date: Mon, 25 Nov 2024 18:16:59 +0100 Subject: [PATCH 3/6] Bugfix: coderabbitai comments: - Improve precision handling in amount calculation. - Add validator address validation. --- injective_functions/staking/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/injective_functions/staking/__init__.py b/injective_functions/staking/__init__.py index ed19172..b946626 100644 --- a/injective_functions/staking/__init__.py +++ b/injective_functions/staking/__init__.py @@ -28,6 +28,9 @@ async def compound_rewards(self, validator_address: str) -> Dict: :return: Transaction result """ try: + if not validator_address.startswith("injvaloper"): + raise ValueError("Invalid validator address format") + # Step 1: Fetch the initial INJ balance balance_response = await self.chain_client.client.get_bank_balance( address=self.chain_client.address.to_acc_bech32(), @@ -62,7 +65,7 @@ async def compound_rewards(self, validator_address: str) -> Dict: delegate_msg = self.chain_client.composer.MsgDelegate( delegator_address=self.chain_client.address.to_acc_bech32(), validator_address=validator_address, - amount=rewards_to_stake / (10 ** 18), + amount=rewards_to_stake / Decimal('1e18'), ) delegate_response = await self.chain_client.build_and_broadcast_tx(delegate_msg) From 1ae28fd49e954845d9b0de700fe76d3eb806dc50 Mon Sep 17 00:00:00 2001 From: 0xDEFSER Date: Mon, 25 Nov 2024 18:34:22 +0100 Subject: [PATCH 4/6] Readability: clean up logic path to cover each discrete case --- injective_functions/staking/__init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/injective_functions/staking/__init__.py b/injective_functions/staking/__init__.py index b946626..a8eb1a6 100644 --- a/injective_functions/staking/__init__.py +++ b/injective_functions/staking/__init__.py @@ -50,15 +50,13 @@ async def compound_rewards(self, validator_address: str) -> Dict: # Step 4: Calculate the withdrawn rewards rewards_to_stake = updated_balance - initial_balance - if rewards_to_stake <= 0: - if rewards_to_stake < 0: - # Specific error for negative rewards - return { - "success": False, - "error": f"Rewards ({rewards_to_stake}) are lower than gas fees, resulting in a negative net " - f"reward." - } - # Generic error for zero rewards + if rewards_to_stake < 0: + return { + "success": False, + "error": f"Rewards ({rewards_to_stake}) are lower than gas fees, resulting in a negative net reward." + } + + if rewards_to_stake == 0: return {"success": False, "error": "No rewards available to compound."} # Step 5: Restake the rewards From 4f1e102cb34a777d4ed0e9f5d6f5558f16f5c9a8 Mon Sep 17 00:00:00 2001 From: 0xDEFSER Date: Mon, 25 Nov 2024 23:23:53 +0100 Subject: [PATCH 5/6] Resolve comments: creating a constant for the 1e18 decimal conversion and put compound_rewards schema only inside staking_schema.json --- injective_functions/function_schemas.json | 14 -------------- injective_functions/functions_schemas.json | 14 -------------- injective_functions/staking/__init__.py | 4 +++- 3 files changed, 3 insertions(+), 29 deletions(-) diff --git a/injective_functions/function_schemas.json b/injective_functions/function_schemas.json index ff8530f..81b8721 100644 --- a/injective_functions/function_schemas.json +++ b/injective_functions/function_schemas.json @@ -102,20 +102,6 @@ "required": ["validator_address", "amount"] } }, - { - "name": "compound_rewards", - "description": "Automatically reinvest your staking rewards with a specific validator to increase your staked amount.", - "parameters": { - "type": "object", - "properties": { - "validator_address": { - "type": "string", - "description": "Validator address you want to compound your rewards with." - } - }, - "required": ["validator_address"] - } - }, { "name": "transfer_funds", "description": "Transfer funds to another Injective address", diff --git a/injective_functions/functions_schemas.json b/injective_functions/functions_schemas.json index 0ac10de..891eabf 100644 --- a/injective_functions/functions_schemas.json +++ b/injective_functions/functions_schemas.json @@ -778,20 +778,6 @@ ] } }, - { - "name": "compound_rewards", - "description": "Automatically reinvest your staking rewards with a specific validator to increase your staked amount.", - "parameters": { - "type": "object", - "properties": { - "validator_address": { - "type": "string", - "description": "Validator address you want to compound your rewards with." - } - }, - "required": ["validator_address"] - } - }, { "name": "create_denom", "description": "Create a new token denomination", diff --git a/injective_functions/staking/__init__.py b/injective_functions/staking/__init__.py index a8eb1a6..b3291d3 100644 --- a/injective_functions/staking/__init__.py +++ b/injective_functions/staking/__init__.py @@ -8,6 +8,8 @@ class InjectiveStaking(InjectiveBase): + DECIMAL_CONVERSION_1E18 = Decimal('1e18') + def __init__(self, chain_client) -> None: # Initializes the network and the composer super().__init__(chain_client) @@ -63,7 +65,7 @@ async def compound_rewards(self, validator_address: str) -> Dict: delegate_msg = self.chain_client.composer.MsgDelegate( delegator_address=self.chain_client.address.to_acc_bech32(), validator_address=validator_address, - amount=rewards_to_stake / Decimal('1e18'), + amount=rewards_to_stake / self.DECIMAL_CONVERSION_1E18, ) delegate_response = await self.chain_client.build_and_broadcast_tx(delegate_msg) From b61cab8479ea30bcd11561bf69d3f068b4e299f6 Mon Sep 17 00:00:00 2001 From: 0xDEFSER Date: Tue, 26 Nov 2024 16:47:39 +0100 Subject: [PATCH 6/6] Resolve comments: make use of pyinjective.constant ADDITIONAL_CHAIN_FORMAT_DECIMALS and INJ_DENOM --- injective_functions/staking/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/injective_functions/staking/__init__.py b/injective_functions/staking/__init__.py index b3291d3..adc5140 100644 --- a/injective_functions/staking/__init__.py +++ b/injective_functions/staking/__init__.py @@ -1,5 +1,8 @@ import asyncio from decimal import Decimal + +from pyinjective.constant import ADDITIONAL_CHAIN_FORMAT_DECIMALS, INJ_DENOM + from injective_functions.base import InjectiveBase from typing import Dict @@ -8,8 +11,6 @@ class InjectiveStaking(InjectiveBase): - DECIMAL_CONVERSION_1E18 = Decimal('1e18') - def __init__(self, chain_client) -> None: # Initializes the network and the composer super().__init__(chain_client) @@ -36,7 +37,7 @@ async def compound_rewards(self, validator_address: str) -> Dict: # Step 1: Fetch the initial INJ balance balance_response = await self.chain_client.client.get_bank_balance( address=self.chain_client.address.to_acc_bech32(), - denom="inj" + denom=INJ_DENOM ) initial_balance = Decimal(balance_response.balance.amount) @@ -48,7 +49,7 @@ async def compound_rewards(self, validator_address: str) -> Dict: withdraw_response = await self.chain_client.build_and_broadcast_tx(withdraw_msg) # Step 3: Wait for the balance to update - updated_balance = await self.wait_for_balance_update(old_balance=initial_balance, denom="inj") + updated_balance = await self.wait_for_balance_update(old_balance=initial_balance, denom=INJ_DENOM) # Step 4: Calculate the withdrawn rewards rewards_to_stake = updated_balance - initial_balance @@ -65,7 +66,7 @@ async def compound_rewards(self, validator_address: str) -> Dict: delegate_msg = self.chain_client.composer.MsgDelegate( delegator_address=self.chain_client.address.to_acc_bech32(), validator_address=validator_address, - amount=rewards_to_stake / self.DECIMAL_CONVERSION_1E18, + amount=rewards_to_stake / Decimal(f"1e{ADDITIONAL_CHAIN_FORMAT_DECIMALS}"), ) delegate_response = await self.chain_client.build_and_broadcast_tx(delegate_msg)