Repository: rotki/rotki

Issue:
Show Binance Earn balances and Liquidity swap pools balances
## Abstract

Some users asked for include of Binance Earn and Liquidity swap pools balances.

## Task

Investigate how to detect and display them in Rotki


Discussion:

Resolving patch:
diff --git a/rotkehlchen/exchanges/binance.py b/rotkehlchen/exchanges/binance.py
--- a/rotkehlchen/exchanges/binance.py
+++ b/rotkehlchen/exchanges/binance.py
@@ -78,6 +78,7 @@
     'lending/daily/token/position',
     'lending/daily/product/list',
     'lending/union/account',
+    'bswap/liquidity',
 )
 
 FAPI_METHODS = (
@@ -509,6 +510,11 @@ def _query_lending_balances(
             self,
             balances: DefaultDict[Asset, Balance],
     ) -> DefaultDict[Asset, Balance]:
+        """Queries binance lending balances and if any found adds them to `balances`
+
+        May raise:
+        - RemoteError
+        """
         data = self.api_query_dict('sapi', 'lending/union/account')
         positions = data.get('positionAmountVos', None)
         if positions is None:
@@ -566,6 +572,11 @@ def _query_cross_collateral_futures_balances(
             self,
             balances: DefaultDict[Asset, Balance],
     ) -> DefaultDict[Asset, Balance]:
+        """Queries binance collateral future balances and if any found adds them to `balances`
+
+        May raise:
+        - RemoteError
+        """
         futures_response = self.api_query_dict('sapi', 'futures/loan/wallet')
         try:
             cross_collaterals = futures_response['crossCollaterals']
@@ -622,6 +633,11 @@ def _query_margined_futures_balances(
             api_type: Literal['fapi', 'dapi'],
             balances: DefaultDict[Asset, Balance],
     ) -> DefaultDict[Asset, Balance]:
+        """Queries binance margined future balances and if any found adds them to `balances`
+
+        May raise:
+        - RemoteError
+        """
         try:
             response = self.api_query_list(api_type, 'balance')
         except BinancePermissionError as e:
@@ -680,6 +696,85 @@ def _query_margined_futures_balances(
 
         return balances
 
+    def _query_pools_balances(
+            self,
+            balances: DefaultDict[Asset, Balance],
+    ) -> DefaultDict[Asset, Balance]:
+        """Queries binance pool balances and if any found adds them to `balances`
+
+        May raise:
+        - RemoteError
+        """
+
+        def process_pool_asset(asset_name: str, asset_amount: FVal) -> None:
+            if asset_amount == ZERO:
+                return None
+
+            try:
+                asset = asset_from_binance(asset_name)
+            except UnsupportedAsset as e:
+                self.msg_aggregator.add_warning(
+                    f'Found unsupported {self.name} asset {asset_name}. '
+                    f'Ignoring its {self.name} pool balance query. {str(e)}',
+                )
+                return None
+            except UnknownAsset as e:
+                self.msg_aggregator.add_warning(
+                    f'Found unknown {self.name} asset {asset_name}. '
+                    f'Ignoring its {self.name} pool balance query. {str(e)}',
+                )
+                return None
+            except DeserializationError as e:
+                self.msg_aggregator.add_error(
+                    f'{self.name} balance deserialization error '
+                    f'for asset {asset_name}: {str(e)}. Skipping entry.',
+                )
+                return None
+
+            try:
+                usd_price = Inquirer().find_usd_price(asset)
+            except RemoteError as e:
+                self.msg_aggregator.add_error(
+                    f'Error processing {self.name} balance entry due to inability to '
+                    f'query USD price: {str(e)}. Skipping {self.name} pool balance entry',
+                )
+                return None
+
+            balances[asset] += Balance(
+                amount=asset_amount,
+                usd_value=asset_amount * usd_price,
+            )
+            return None
+
+        try:
+            response = self.api_query('sapi', 'bswap/liquidity')
+        except BinancePermissionError as e:
+            log.warning(
+                f'Insufficient permission to query {self.name} pool balances.'
+                f'Skipping query. Response details: {str(e)}',
+            )
+            return balances
+
+        try:
+            for entry in response:
+                for asset_name, asset_amount in entry['share']['asset'].items():
+                    process_pool_asset(asset_name, FVal(asset_amount))
+        except (KeyError, AttributeError) as e:
+            self.msg_aggregator.add_error(
+                f'At {self.name} pool balances got unexpected data format. '
+                f'Skipping them in the balance query. Check logs for details',
+            )
+            if isinstance(e, KeyError):
+                msg = f'Missing key {str(e)}'
+            else:
+                msg = str(e)
+            log.error(
+                f'Unexpected data format returned by {self.name} pools. '
+                f'Data: {response}. Error: {msg}',
+            )
+
+        return balances
+
     @protect_with_lock()
     @cache_response_timewise()
     def query_balances(self) -> ExchangeQueryBalances:
@@ -692,6 +787,7 @@ def query_balances(self) -> ExchangeQueryBalances:
                 returned_balances = self._query_cross_collateral_futures_balances(returned_balances)  # noqa: E501
                 returned_balances = self._query_margined_futures_balances('fapi', returned_balances)  # noqa: E501
                 returned_balances = self._query_margined_futures_balances('dapi', returned_balances)  # noqa: E501
+                returned_balances = self._query_pools_balances(returned_balances)
 
         except RemoteError as e:
             msg = (


Test patch:
diff --git a/rotkehlchen/tests/exchanges/test_binance.py b/rotkehlchen/tests/exchanges/test_binance.py
--- a/rotkehlchen/tests/exchanges/test_binance.py
+++ b/rotkehlchen/tests/exchanges/test_binance.py
@@ -10,7 +10,7 @@
 
 from rotkehlchen.assets.asset import Asset
 from rotkehlchen.assets.converters import UNSUPPORTED_BINANCE_ASSETS, asset_from_binance
-from rotkehlchen.constants.assets import A_BNB, A_BTC, A_ETH, A_USDT
+from rotkehlchen.constants.assets import A_BNB, A_BTC, A_ETH, A_USDT, A_WBTC
 from rotkehlchen.constants.misc import ZERO
 from rotkehlchen.errors import RemoteError, UnknownAsset, UnsupportedAsset
 from rotkehlchen.exchanges.binance import (
@@ -202,12 +202,13 @@ def test_binance_query_balances_include_features(function_scope_binance):
         balances, msg = binance.query_balances()
 
     assert msg == ''
-    assert len(balances) == 5
-    assert balances[A_BTC].amount == FVal('4723847.39208129')
+    assert len(balances) == 6
+    assert balances[A_BTC].amount == FVal('4723849.39208129')
     assert balances[A_ETH].amount == FVal('4763368.68006011')
     assert balances[A_BUSD].amount == FVal('7.49283144')
     assert balances[A_USDT].amount == FVal('201.01')
     assert balances[A_DOT].amount == FVal('500.55')
+    assert balances[A_WBTC].amount == FVal('2.1')
 
     warnings = binance.msg_aggregator.consume_warnings()
     assert len(warnings) == 2
diff --git a/rotkehlchen/tests/utils/exchanges.py b/rotkehlchen/tests/utils/exchanges.py
--- a/rotkehlchen/tests/utils/exchanges.py
+++ b/rotkehlchen/tests/utils/exchanges.py
@@ -179,6 +179,43 @@
     ]
 }"""
 
+BINANCE_POOL_BALANCES_RESPONSE = """[
+    {
+        "poolId": 2,
+        "poolName": "BUSD/USDT",
+        "updateTime": 2,
+        "liquidity": {
+            "USDT": "80",
+            "BUSD": "80"
+        },
+        "share": {
+            "shareAmount": "0",
+            "sharePercentage": "0",
+            "asset": {
+                "BUSD": "0",
+                "USDT": "0"
+            }
+        }
+    },
+    {
+        "poolId": 15,
+        "poolName": "BTC/WBTC",
+        "updateTime": 2,
+        "liquidity": {
+            "BTC": "80",
+            "WBTC": "80"
+        },
+        "share": {
+            "shareAmount": "0",
+            "sharePercentage": "0",
+            "asset": {
+                "BTC": "2",
+                "WBTC": "2.1"
+            }
+        }
+    }
+]"""
+
 BINANCE_USDT_FUTURES_BALANCES_RESPONSE = """[
 {"accountAlias": "foo", "asset": "USDT", "availableBalance": "125.55", "balance": "125.55", "crossUnPnl": "0", "crossWalletBalance": "125.55", "maxWithdrawAmount": "125.55"},
  {"accountAlias": "foo", "asset": "BNB", "availableBalance": "0", "balance": "0", "crossUnPnl": "0", "crossWalletBalance": "0", "maxWithdrawAmount": "0"},
@@ -265,6 +302,8 @@ def mock_binance_balance_response(url):
         return MockResponse(200, BINANCE_USDT_FUTURES_BALANCES_RESPONSE)
     if 'https://dapi' in url:
         return MockResponse(200, BINANCE_COIN_FUTURES_BALANCES_RESPONSE)
+    if 'bswap/liquidity' in url:
+        return MockResponse(200, BINANCE_POOL_BALANCES_RESPONSE)
 
     # else
     return MockResponse(200, BINANCE_BALANCES_RESPONSE)
@@ -280,6 +319,8 @@ def mock_binance_asset_return(url, *args):  # pylint: disable=unused-argument
             response = '[]'
         elif 'https://dapi' in url:
             response = '[]'
+        elif 'bswap/liquidity' in url:
+            response = '[]'
         else:
             response = BINANCE_BALANCES_RESPONSE
         return MockResponse(200, response)