Repository: rotki/rotki

Issue:
Add gemini earn balances
## Abstract

A user in discord who has a Gemini account mentioned that they can't see their Gemini earn balances.

It appears that gemini has some kind of special staking balances called earn.

They have them under a different endpoint which we are not currently querying: https://docs.gemini.com/rest-api/#get-earn-balances

## Task

Include gemini earn balances when querying gemini
Add gemini earn balances
## Abstract

A user in discord who has a Gemini account mentioned that they can't see their Gemini earn balances.

It appears that gemini has some kind of special staking balances called earn.

They have them under a different endpoint which we are not currently querying: https://docs.gemini.com/rest-api/#get-earn-balances

## Task

Include gemini earn balances when querying gemini


Discussion:

Resolving patch:
diff --git a/rotkehlchen/exchanges/gemini.py b/rotkehlchen/exchanges/gemini.py
--- a/rotkehlchen/exchanges/gemini.py
+++ b/rotkehlchen/exchanges/gemini.py
@@ -178,7 +178,7 @@ def _query_continuously(
         url = f'{self.base_uri}{v_endpoint}'
         retries_left = QUERY_RETRY_TIMES
         while retries_left > 0:
-            if endpoint in ('mytrades', 'balances', 'transfers', 'roles'):
+            if endpoint in ('mytrades', 'balances', 'transfers', 'roles', 'balances/earn'):
                 # private endpoints
                 timestamp = str(ts_now_in_ms())
                 payload = {'request': v_endpoint, 'nonce': timestamp}
@@ -252,7 +252,7 @@ def _private_api_query(  # pylint: disable=no-self-use
     @overload
     def _private_api_query(  # pylint: disable=no-self-use
             self,
-            endpoint: Literal['balances', 'mytrades', 'transfers'],
+            endpoint: Literal['balances', 'mytrades', 'transfers', 'balances/earn'],
             options: Optional[Dict[str, Any]] = None,
     ) -> List[Any]:
         ...
@@ -305,6 +305,7 @@ def _private_api_query(
     def query_balances(self) -> ExchangeQueryBalances:
         try:
             balances = self._private_api_query('balances')
+            balances.extend(self._private_api_query('balances/earn'))
         except (GeminiPermissionError, RemoteError) as e:
             msg = f'Gemini API request failed. {str(e)}'
             log.error(msg)
@@ -313,7 +314,11 @@ def query_balances(self) -> ExchangeQueryBalances:
         returned_balances: Dict[Asset, Balance] = {}
         for entry in balances:
             try:
-                amount = deserialize_asset_amount(entry['amount'])
+                balance_type = entry['type']
+                if balance_type == 'exchange':
+                    amount = deserialize_asset_amount(entry['amount'])
+                else:  # should be 'Earn'
+                    amount = deserialize_asset_amount(entry['balance'])
                 # ignore empty balances
                 if amount == ZERO:
                     continue
@@ -323,8 +328,8 @@ def query_balances(self) -> ExchangeQueryBalances:
                     usd_price = Inquirer().find_usd_price(asset=asset)
                 except RemoteError as e:
                     self.msg_aggregator.add_error(
-                        f'Error processing gemini balance result due to inability to '
-                        f'query USD price: {str(e)}. Skipping balance entry',
+                        f'Error processing gemini {balance_type} balance result due to '
+                        f'inability to query USD price: {str(e)}. Skipping balance entry',
                     )
                     continue
 
@@ -340,8 +345,8 @@ def query_balances(self) -> ExchangeQueryBalances:
                 continue
             except UnsupportedAsset as e:
                 self.msg_aggregator.add_warning(
-                    f'Found gemini balance result with unsupported asset '
-                    f'{e.asset_name}. Ignoring it.',
+                    f'Found gemini {balance_type} balance result with unsupported '
+                    f'asset {e.asset_name}. Ignoring it.',
                 )
                 continue
             except (DeserializationError, KeyError) as e:
@@ -349,11 +354,11 @@ def query_balances(self) -> ExchangeQueryBalances:
                 if isinstance(e, KeyError):
                     msg = f'Missing key entry for {msg}.'
                 self.msg_aggregator.add_error(
-                    'Error processing a gemini balance. Check logs '
-                    'for details. Ignoring it.',
+                    f'Error processing a gemini {balance_type} balance. Check logs '
+                    f'for details. Ignoring it.',
                 )
                 log.error(
-                    'Error processing a gemini balance',
+                    f'Error processing a gemini {balance_type} balance',
                     error=msg,
                 )
                 continue


Test patch: