Repository: rotki/rotki

Issue:
Coinbase trade query runs into infinite loop
## Problem Definition

Coinbase trades query hangs because it runs into an infinite loop when one pagination request is attempted.

The problem is that the variable `json_ret` is never re-assigned.



Discussion:
Nice find @coinyon 

Resolving patch:
diff --git a/rotkehlchen/exchanges/coinbase.py b/rotkehlchen/exchanges/coinbase.py
--- a/rotkehlchen/exchanges/coinbase.py
+++ b/rotkehlchen/exchanges/coinbase.py
@@ -290,22 +290,21 @@ def _api_query(
 
         # If we got pagination and this is the first query, gather all the subsequent queries
         if 'pagination' in json_ret and not pagination_next_uri and not ignore_pagination:
-            while True:
-                if 'next_uri' not in json_ret['pagination']:
-                    raise RemoteError(f'Coinbase json response contained no "next_uri" key')
-
-                next_uri = json_ret['pagination']['next_uri']
-                if not next_uri:
-                    # As per the docs: https://developers.coinbase.com/api/v2?python#pagination
-                    # once we get an empty next_uri we are done
-                    break
-
-                additional_data = self._api_query(
-                    endpoint=endpoint,
-                    options=options,
-                    pagination_next_uri=next_uri,
-                )
-                final_data.extend(additional_data)
+            if 'next_uri' not in json_ret['pagination']:
+                raise RemoteError(f'Coinbase json response contained no "next_uri" key')
+
+            next_uri = json_ret['pagination']['next_uri']
+            if not next_uri:
+                # As per the docs: https://developers.coinbase.com/api/v2?python#pagination
+                # once we get an empty next_uri we are done
+                return final_data
+
+            additional_data = self._api_query(
+                endpoint=endpoint,
+                options=options,
+                pagination_next_uri=next_uri,
+            )
+            final_data.extend(additional_data)
 
         return final_data
 


Test patch:
diff --git a/rotkehlchen/tests/exchanges/test_coinbase.py b/rotkehlchen/tests/exchanges/test_coinbase.py
--- a/rotkehlchen/tests/exchanges/test_coinbase.py
+++ b/rotkehlchen/tests/exchanges/test_coinbase.py
@@ -486,7 +486,9 @@ def query_coinbase_and_test(
         coinbase,
         query_fn_name,
         buys_response=BUYS_RESPONSE,
+        buys_paginated_end=BUYS_RESPONSE,
         sells_response=SELLS_RESPONSE,
+        sells_paginated_end=SELLS_RESPONSE,
         deposits_response=DEPOSITS_RESPONSE,
         withdrawals_response=WITHDRAWALS_RESPONSE,
         expected_warnings_num=0,
@@ -496,9 +498,15 @@ def query_coinbase_and_test(
 ):
     def mock_coinbase_query(url):  # pylint: disable=unused-argument
         if 'buys' in url:
-            return MockResponse(200, buys_response)
+            if 'next-page' in url:
+                return MockResponse(200, buys_paginated_end)
+            else:
+                return MockResponse(200, buys_response)
         elif 'sells' in url:
-            return MockResponse(200, sells_response)
+            if 'next-page' in url:
+                return MockResponse(200, sells_paginated_end)
+            else:
+                return MockResponse(200, sells_response)
         elif 'deposits' in url:
             return MockResponse(200, deposits_response)
         elif 'withdrawals' in url:
@@ -518,7 +526,7 @@ def mock_coinbase_query(url):  # pylint: disable=unused-argument
 
     errors = coinbase.msg_aggregator.consume_errors()
     warnings = coinbase.msg_aggregator.consume_warnings()
-    if expected_errors_num == 0 and expected_warnings_num == 0:
+    if expected_errors_num == 0 and expected_warnings_num == 0 and expected_actions_num == 1:
         assert len(actions) == 2
         assert len(errors) == 0
         assert len(warnings) == 0
@@ -652,6 +660,32 @@ def test_coinbase_query_trade_history_unexpected_data(function_scope_coinbase):
     )
 
 
+def test_coinbase_query_trade_history_paginated(function_scope_coinbase):
+    """Test that coinbase trade history query can deal with paginated response"""
+    coinbase = function_scope_coinbase
+    coinbase.cache_ttl_secs = 0
+
+    paginated_buys_response = BUYS_RESPONSE.replace(
+        '"next_uri": null',
+        '"next_uri": "/v2/buys/?next-page"',
+    )
+
+    paginated_sells_response = SELLS_RESPONSE.replace(
+        '"next_uri": null',
+        '"next_uri": "/v2/sells/?next-page"',
+    )
+
+    query_coinbase_and_test(
+        coinbase=coinbase,
+        query_fn_name='query_online_trade_history',
+        expected_warnings_num=0,
+        expected_errors_num=0,
+        expected_actions_num=4,
+        buys_response=paginated_buys_response,
+        sells_response=paginated_sells_response,
+    )
+
+
 def test_coinbase_query_deposit_withdrawals(function_scope_coinbase):
     """Test that coinbase deposit/withdrawals history query works fine for the happy path"""
     coinbase = function_scope_coinbase