Repository: rotki/rotki

Issue:
Support GMX v1 in arbitrum.
## Abstract

Support the GMX protocol in arbitrum: https://gmx.io/

## Task

- [x] Support querying GMX positions (#7674)
- [x] Decode GMX events (#7309 and #7394) 
- [ ] Support querying GMX staking balances


Discussion:

Resolving patch:
diff --git a/rotkehlchen/chain/arbitrum_one/modules/gmx/balances.py b/rotkehlchen/chain/arbitrum_one/modules/gmx/balances.py
--- a/rotkehlchen/chain/arbitrum_one/modules/gmx/balances.py
+++ b/rotkehlchen/chain/arbitrum_one/modules/gmx/balances.py
@@ -9,6 +9,7 @@
 from rotkehlchen.chain.ethereum.utils import token_normalized_value_decimals
 from rotkehlchen.chain.evm.types import string_to_evm_address
 from rotkehlchen.constants import ZERO
+from rotkehlchen.constants.assets import A_GMX
 from rotkehlchen.constants.resolver import evm_address_to_identifier
 from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
 from rotkehlchen.errors.misc import RemoteError
@@ -18,7 +19,7 @@
 from rotkehlchen.types import ChainID, ChecksumEvmAddress, EvmTokenKind
 from rotkehlchen.utils.misc import get_chunks
 
-from .constants import CPT_GMX, GMX_READER, GMX_USD_DECIMALS, GMX_VAULT_ADDRESS
+from .constants import CPT_GMX, GMX_READER, GMX_STAKING_REWARD, GMX_USD_DECIMALS, GMX_VAULT_ADDRESS
 
 if TYPE_CHECKING:
     from rotkehlchen.chain.arbitrum_one.node_inquirer import ArbitrumOneInquirer
@@ -37,6 +38,7 @@ def __init__(self, database: 'DBHandler', evm_inquirer: 'ArbitrumOneInquirer'):
             counterparty=CPT_GMX,
             deposit_event_types={(HistoryEventType.DEPOSIT, HistoryEventSubType.DEPOSIT_ASSET)},
         )
+        self.gmx = A_GMX.resolve_to_evm_token()
 
     def _extract_unique_deposits(self) -> dict['ChecksumEvmAddress', set[tuple[str, str, bool]]]:
         """
@@ -68,9 +70,9 @@ def _extract_unique_deposits(self) -> dict['ChecksumEvmAddress', set[tuple[str,
             unique_deposits[address] = positions
         return unique_deposits
 
-    def query_balances(self) -> 'BalancesType':
+    def query_position_balances(self) -> 'BalancesType':
         """
-        Query balances for GMX open positions.
+        Query balances for GMX open positions and returns it.
 
         The position query uses as argument the address of the user and 3 arrays of collaterals
         used, index tokens used and if the positions are shorts or longs. The arrays must have
@@ -83,7 +85,7 @@ def query_balances(self) -> 'BalancesType':
         deposit (only its USD value) and a delta value that is how much the position has earned
         (also in USD). We sum collateral value + delta to get the current value of the position.
         """
-        balances: BalancesType = defaultdict(lambda: defaultdict(Balance))
+        balances: 'BalancesType' = defaultdict(lambda: defaultdict(Balance))
         unique_deposits = self._extract_unique_deposits()
         if len(unique_deposits) == 0:
             return balances
@@ -160,3 +162,44 @@ def query_balances(self) -> 'BalancesType':
                 )
 
         return balances
+
+    def query_staking_balances(self, balances: 'BalancesType') -> 'BalancesType':
+        """
+        Query staked balances for GMX. It modifies the `balances` argument to include
+        the staking balances and returns it.
+        - This method queries for staked GMX
+        - staked GLP (fsGLP) is detected as a token already by the app and price is
+        queried correctly
+        - sbGMX is also tracked as a token
+        - esGMX (vested GMX) is tracked as a token as is not transferable. No price for this asset
+        """
+        addresses_events = self.addresses_with_activity(
+            products=None,
+            event_types={(HistoryEventType.STAKING, HistoryEventSubType.DEPOSIT_ASSET)},
+        )
+        if len(addresses_events) == 0:
+            return balances
+
+        reward_contract = self.evm_inquirer.contracts.contract(GMX_STAKING_REWARD)
+        gmx_price = Inquirer.find_usd_price(asset=A_GMX)
+        for user_address in addresses_events:
+            staked_amount_raw = reward_contract.call(
+                node_inquirer=self.evm_inquirer,
+                method_name='stakedAmounts',
+                arguments=[user_address],
+            )
+            amount = token_normalized_value_decimals(
+                token_amount=staked_amount_raw,
+                token_decimals=18,  # GMX has 18 decimals
+            )
+            balances[user_address][self.gmx] += Balance(
+                amount=amount,
+                usd_value=amount * gmx_price,
+            )
+
+        return balances
+
+    def query_balances(self) -> 'BalancesType':
+        """Query balances for GMX open positions"""
+        balances = self.query_position_balances()
+        return self.query_staking_balances(balances)
diff --git a/rotkehlchen/chain/arbitrum_one/modules/gmx/constants.py b/rotkehlchen/chain/arbitrum_one/modules/gmx/constants.py
--- a/rotkehlchen/chain/arbitrum_one/modules/gmx/constants.py
+++ b/rotkehlchen/chain/arbitrum_one/modules/gmx/constants.py
@@ -9,6 +9,7 @@
 GMX_POSITION_ROUTER: Final = string_to_evm_address('0xb87a436B93fFE9D75c5cFA7bAcFff96430b09868')
 GMX_REWARD_ROUTER: Final = string_to_evm_address('0x159854e14A862Df9E39E1D128b8e5F70B4A3cE9B')
 GMX_READER: Final = string_to_evm_address('0x22199a49A999c351eF7927602CFB187ec3cae489')
+GMX_STAKING_REWARD: Final = string_to_evm_address('0x908C4D94D34924765f1eDc22A1DD098397c59dD4')
 SWAP_TOPIC: Final = b'\xcd8)\xa3\x81=\xc3\xcd\xd1\x88\xfd=\x01\xdc\xf3&\x8c\x16\xbe/\xdd-\xd2\x1d\x06eA\x88\x16\xe4`b'  # noqa: E501
 CREATE_INCREASE_TOPIC: Final = b'Re\xbcIR\xda@&3\xb3\xfc5\xf6z\xb4$T\x93\xa0\xab\x94\xdd\x8a\xb1#f|\x8dE\xa4H\\'  # noqa: E501
 EXECUTE_DECREASE_TOPIC: Final = b'!C\\[a\x8dw\xff6W\x14\x0c\xd31\x8e,\xff\xae\xbc^\x0e\x1bs\x18\xf5j\x9b\xa4\x04L>\xd2'  # noqa: E501


Test patch:
diff --git a/rotkehlchen/tests/unit/test_protocol_balances.py b/rotkehlchen/tests/unit/test_protocol_balances.py
--- a/rotkehlchen/tests/unit/test_protocol_balances.py
+++ b/rotkehlchen/tests/unit/test_protocol_balances.py
@@ -17,12 +17,15 @@
 from rotkehlchen.chain.evm.tokens import TokenBalancesType
 from rotkehlchen.chain.evm.types import string_to_evm_address
 from rotkehlchen.chain.optimism.modules.velodrome.balances import VelodromeBalances
-from rotkehlchen.constants.assets import A_CVX, A_GLM, A_GRT, A_STETH
+from rotkehlchen.constants.assets import A_CVX, A_GLM, A_GMX, A_GRT, A_STETH
 from rotkehlchen.constants.misc import ONE
 from rotkehlchen.constants.resolver import evm_address_to_identifier
 from rotkehlchen.fval import FVal
 from rotkehlchen.tests.utils.arbitrum_one import get_arbitrum_allthatnode
-from rotkehlchen.tests.utils.ethereum import get_decoded_events_of_transaction
+from rotkehlchen.tests.utils.ethereum import (
+    get_decoded_events_of_transaction,
+    wait_until_all_nodes_connected,
+)
 from rotkehlchen.types import (
     ChainID,
     ChecksumEvmAddress,
@@ -356,6 +359,42 @@ def test_gmx_balances(
     }
 
 
+@pytest.mark.vcr(filter_query_parameters=['apikey'])
+@pytest.mark.parametrize('arbitrum_one_accounts', [['0x3D3db31AdbCCB6a4e5aE94ec0701d361E147FA6A']])
+@pytest.mark.parametrize('arbitrum_one_manager_connect_at_start', [(get_arbitrum_allthatnode(weight=ONE, owned=True),)])  # noqa: E501
+@pytest.mark.parametrize('should_mock_current_price_queries', [False])
+def test_gmx_balances_staking(
+        arbitrum_one_inquirer: 'ArbitrumOneInquirer',
+        arbitrum_one_transaction_decoder: 'OptimismTransactionDecoder',
+        arbitrum_one_accounts: list[ChecksumEvmAddress],
+        arbitrum_one_manager_connect_at_start,
+        inquirer: 'Inquirer',  # pylint: disable=unused-argument
+) -> None:
+    """Test the balance query for staked GMX. It adds a staking event and then queries the
+    balances for that address.
+    """
+    wait_until_all_nodes_connected(
+        connect_at_start=arbitrum_one_manager_connect_at_start,
+        evm_inquirer=arbitrum_one_inquirer,
+    )
+    get_decoded_events_of_transaction(
+        evm_inquirer=arbitrum_one_inquirer,
+        database=arbitrum_one_transaction_decoder.database,
+        tx_hash=deserialize_evm_tx_hash('0x25160bf17e5a77935c7661933c045739dba44606859a20f00f187ef291e56a8f'),
+    )
+    balances_inquirer = GmxBalances(
+        database=arbitrum_one_transaction_decoder.database,
+        evm_inquirer=arbitrum_one_inquirer,
+    )
+    balances = balances_inquirer.query_balances()
+    assert balances[arbitrum_one_accounts[0]] == {
+        A_GMX: Balance(
+            amount=FVal('4.201981641893733976'),
+            usd_value=FVal('164.46556146372074782064'),
+        ),
+    }
+
+
 @pytest.mark.vcr(filter_query_parameters=['apikey'])
 @pytest.mark.parametrize('ethereum_accounts', [[
     '0xecdED8b1c603cF21299835f1DFBE37f10F2a29Af', '0x8Cbac427B6967d5d84Ec4230081e2763AB3A8C92',