1
+ """
2
+ Test suite for EVM ETH price methods to achieve 100% coverage.
3
+
4
+ Tests the get_eth_price method and its helper functions.
5
+ """
6
+
7
+ import time
8
+ from unittest .mock import AsyncMock , MagicMock , patch
9
+
10
+ import pytest
11
+
12
+ from thegraph_token_api .evm import EVMTokenAPI
13
+ from thegraph_token_api .price_utils import create_price_cache
14
+ from thegraph_token_api .types import NetworkId , Protocol
15
+
16
+
17
+ class TestEVMGetEthPrice :
18
+ """Test get_eth_price method and helpers."""
19
+
20
+ @pytest .mark .asyncio
21
+ async def test_get_eth_price_basic (self ):
22
+ """Test basic ETH price fetching."""
23
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
24
+
25
+ # Mock the internal methods
26
+ with patch .object (evm_api , "_fetch_eth_usdc_swaps" ) as mock_fetch , \
27
+ patch .object (evm_api , "_extract_eth_prices" ) as mock_extract :
28
+
29
+ # Mock swap data
30
+ mock_fetch .return_value = [{"swap" : "data" }] * 10
31
+ mock_extract .return_value = [3500.0 , 3501.0 , 3499.0 , 3502.0 , 3498.0 ]
32
+
33
+ price = await evm_api .get_eth_price ()
34
+
35
+ assert price is not None
36
+ assert isinstance (price , float )
37
+ assert 3498 <= price <= 3502 # Should be around median
38
+
39
+ @pytest .mark .asyncio
40
+ async def test_get_eth_price_with_cache (self ):
41
+ """Test ETH price with cache hit (lines 729-730)."""
42
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
43
+
44
+ # Set up fresh cache
45
+ mock_stats = {
46
+ "price" : 3500.0 ,
47
+ "mean_price" : 3500.0 ,
48
+ "std_deviation" : 10.0 ,
49
+ "confidence" : 0.9 ,
50
+ "trades_analyzed" : 50 ,
51
+ "timestamp" : time .time ()
52
+ }
53
+ evm_api ._eth_price_cache = create_price_cache (3500.0 , mock_stats )
54
+
55
+ # Get price - should use cache
56
+ price = await evm_api .get_eth_price ()
57
+ assert price == 3500.0
58
+
59
+ # Get price with stats - should use cache
60
+ stats = await evm_api .get_eth_price (include_stats = True )
61
+ assert stats == mock_stats
62
+
63
+ @pytest .mark .asyncio
64
+ async def test_get_eth_price_retry_logic (self ):
65
+ """Test ETH price retry logic (lines 737-750)."""
66
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
67
+
68
+ with patch .object (evm_api , "_fetch_eth_usdc_swaps" ) as mock_fetch , \
69
+ patch .object (evm_api , "_extract_eth_prices" ) as mock_extract :
70
+
71
+ # First 3 attempts return insufficient data, 4th succeeds
72
+ mock_fetch .side_effect = [
73
+ [{"swap" : "data1" }], # Attempt 1
74
+ [{"swap" : "data2" }], # Attempt 2
75
+ [{"swap" : "data3" }], # Attempt 3
76
+ [{"swap" : f"data{ i } " } for i in range (10 )] # Attempt 4
77
+ ]
78
+
79
+ mock_extract .side_effect = [
80
+ [3500.0 ], # Only 1 price (insufficient)
81
+ [3500.0 , 3501.0 ], # Only 2 prices (insufficient)
82
+ [3500.0 , 3501.0 ], # Still insufficient
83
+ [3500.0 + i for i in range (10 )] # Sufficient data
84
+ ]
85
+
86
+ price = await evm_api .get_eth_price ()
87
+
88
+ assert price is not None
89
+ assert mock_fetch .call_count == 4
90
+ assert mock_extract .call_count == 4
91
+
92
+ @pytest .mark .asyncio
93
+ async def test_get_eth_price_all_attempts_fail (self ):
94
+ """Test ETH price when all retry attempts fail (line 750)."""
95
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
96
+
97
+ with patch .object (evm_api , "_fetch_eth_usdc_swaps" ) as mock_fetch , \
98
+ patch .object (evm_api , "_extract_eth_prices" ) as mock_extract :
99
+
100
+ # All attempts return insufficient data
101
+ mock_fetch .return_value = [{"swap" : "data" }]
102
+ mock_extract .return_value = [3500.0 ] # Only 1 price (insufficient)
103
+
104
+ price = await evm_api .get_eth_price ()
105
+
106
+ assert price is None
107
+ assert mock_fetch .call_count == 4 # Should try 4 times
108
+
109
+ @pytest .mark .asyncio
110
+ async def test_get_eth_price_no_stats (self ):
111
+ """Test ETH price when calculate_price_statistics returns None (lines 754-755)."""
112
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
113
+
114
+ with patch .object (evm_api , "_fetch_eth_usdc_swaps" ) as mock_fetch , \
115
+ patch .object (evm_api , "_extract_eth_prices" ) as mock_extract , \
116
+ patch ("thegraph_token_api.evm.PriceCalculator" ) as mock_calc_class :
117
+
118
+ mock_calc = MagicMock ()
119
+ mock_calc .progressive_retry_params .return_value = (100 , 15 )
120
+ mock_calc .calculate_price_statistics .return_value = None # No stats
121
+ mock_calc_class .return_value = mock_calc
122
+
123
+ mock_fetch .return_value = [{"swap" : "data" }] * 10
124
+ mock_extract .return_value = [3500.0 ] * 10
125
+
126
+ price = await evm_api .get_eth_price ()
127
+
128
+ assert price is None
129
+
130
+ @pytest .mark .asyncio
131
+ async def test_get_eth_price_exception_handling (self ):
132
+ """Test ETH price exception handling (lines 763-764)."""
133
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
134
+
135
+ with patch .object (evm_api , "_fetch_eth_usdc_swaps" ) as mock_fetch :
136
+ mock_fetch .side_effect = Exception ("Network error" )
137
+
138
+ price = await evm_api .get_eth_price ()
139
+
140
+ assert price is None
141
+
142
+ @pytest .mark .asyncio
143
+ async def test_get_eth_price_empty_swaps_continue (self ):
144
+ """Test ETH price when swaps is empty (line 743)."""
145
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
146
+
147
+ with patch .object (evm_api , "_fetch_eth_usdc_swaps" ) as mock_fetch , \
148
+ patch .object (evm_api , "_extract_eth_prices" ) as mock_extract :
149
+
150
+ # First attempts return empty swaps, last attempt returns data
151
+ mock_fetch .side_effect = [
152
+ [], # Empty swaps - triggers continue
153
+ [], # Empty swaps - triggers continue
154
+ [], # Empty swaps - triggers continue
155
+ [{"swap" : "data" }] * 10 # Valid data on last attempt
156
+ ]
157
+
158
+ mock_extract .return_value = [3500.0 ] * 5 # Valid prices
159
+
160
+ price = await evm_api .get_eth_price ()
161
+
162
+ assert price is not None
163
+ assert mock_fetch .call_count == 4
164
+ # Extract should only be called once (for the non-empty swaps)
165
+ assert mock_extract .call_count == 1
166
+
167
+ @pytest .mark .asyncio
168
+ async def test_fetch_eth_usdc_swaps (self ):
169
+ """Test _fetch_eth_usdc_swaps method (lines 768-793)."""
170
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
171
+
172
+ # Mock swap object with __dict__
173
+ mock_swap1 = MagicMock ()
174
+ mock_swap1 .__dict__ = {"id" : 1 , "amount" : "1000" }
175
+
176
+ # Mock swap as dict
177
+ mock_swap2 = {"id" : 2 , "amount" : "2000" }
178
+
179
+ # Mock swap with neither __dict__ nor dict format
180
+ mock_swap3 = object ()
181
+
182
+ with patch .object (evm_api , "get_swaps" ) as mock_get_swaps :
183
+ mock_get_swaps .return_value = [mock_swap1 , mock_swap2 , mock_swap3 ]
184
+
185
+ result = await evm_api ._fetch_eth_usdc_swaps (100 , 60 )
186
+
187
+ assert len (result ) == 3
188
+ assert result [0 ] == {"id" : 1 , "amount" : "1000" }
189
+ assert result [1 ] == {"id" : 2 , "amount" : "2000" }
190
+ assert result [2 ] == {} # Empty dict for unconvertible object
191
+
192
+ @pytest .mark .asyncio
193
+ async def test_fetch_eth_usdc_swaps_fallback (self ):
194
+ """Test _fetch_eth_usdc_swaps fallback when Uniswap V3 returns no results (lines 777-778)."""
195
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
196
+
197
+ with patch .object (evm_api , "get_swaps" ) as mock_get_swaps :
198
+ # First call (Uniswap V3) returns empty, second call returns data
199
+ mock_get_swaps .side_effect = [
200
+ [], # Uniswap V3 returns empty
201
+ [{"id" : 1 , "swap" : "data" }] # Fallback returns data
202
+ ]
203
+
204
+ result = await evm_api ._fetch_eth_usdc_swaps (100 , 60 )
205
+
206
+ assert len (result ) == 1
207
+ assert mock_get_swaps .call_count == 2
208
+
209
+ # Check first call was with Uniswap V3
210
+ first_call = mock_get_swaps .call_args_list [0 ]
211
+ assert first_call .kwargs ["protocol" ] == Protocol .UNISWAP_V3
212
+
213
+ @pytest .mark .asyncio
214
+ async def test_fetch_eth_usdc_swaps_empty (self ):
215
+ """Test _fetch_eth_usdc_swaps when no swaps found (lines 781-782)."""
216
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
217
+
218
+ with patch .object (evm_api , "get_swaps" ) as mock_get_swaps :
219
+ mock_get_swaps .return_value = None
220
+
221
+ result = await evm_api ._fetch_eth_usdc_swaps (100 , 60 )
222
+
223
+ assert result == []
224
+
225
+ def test_extract_eth_prices (self ):
226
+ """Test _extract_eth_prices method (lines 797-850)."""
227
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
228
+
229
+ swaps = [
230
+ {
231
+ "token0" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" , "decimals" : 18 }, # WETH
232
+ "token1" : {"address" : "0xA0b86a33E7c473D00e05A7B8A4bcF1e50e93D1Af" , "decimals" : 6 }, # USDC
233
+ "amount0" : "1000000000000000000" , # 1 ETH
234
+ "amount1" : "3500000000" , # 3500 USDC
235
+ },
236
+ {
237
+ "token0" : {"address" : "0xA0b86a33E7c473D00e05A7B8A4bcF1e50e93D1Af" , "decimals" : 6 }, # USDC
238
+ "token1" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" , "decimals" : 18 }, # WETH
239
+ "amount0" : "3600000000" , # 3600 USDC
240
+ "amount1" : "1000000000000000000" , # 1 ETH
241
+ },
242
+ # Invalid swap - different tokens
243
+ {
244
+ "token0" : {"address" : "0xInvalid" , "decimals" : 18 },
245
+ "token1" : {"address" : "0xAlsoInvalid" , "decimals" : 6 },
246
+ "amount0" : "1000" ,
247
+ "amount1" : "2000" ,
248
+ },
249
+ # Zero amount swap
250
+ {
251
+ "token0" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" , "decimals" : 18 },
252
+ "token1" : {"address" : "0xA0b86a33E7c473D00e05A7B8A4bcF1e50e93D1Af" , "decimals" : 6 },
253
+ "amount0" : "0" ,
254
+ "amount1" : "3500000000" ,
255
+ },
256
+ # Out of range price
257
+ {
258
+ "token0" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" , "decimals" : 18 },
259
+ "token1" : {"address" : "0xA0b86a33E7c473D00e05A7B8A4bcF1e50e93D1Af" , "decimals" : 6 },
260
+ "amount0" : "1000000000000000000" , # 1 ETH
261
+ "amount1" : "50000000000" , # 50,000 USDC - out of range
262
+ }
263
+ ]
264
+
265
+ prices = evm_api ._extract_eth_prices (swaps )
266
+
267
+ assert len (prices ) == 2
268
+ assert 3500 in prices
269
+ assert 3600 in prices
270
+
271
+ def test_extract_eth_prices_with_outliers (self ):
272
+ """Test _extract_eth_prices with outlier removal (lines 843-848)."""
273
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
274
+
275
+ # Create 6 swaps with one outlier
276
+ swaps = []
277
+ for i , price in enumerate ([3500 , 3501 , 3502 , 3503 , 3504 , 10000 ]): # 10000 is outlier
278
+ swaps .append ({
279
+ "token0" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" , "decimals" : 18 },
280
+ "token1" : {"address" : "0xA0b86a33E7c473D00e05A7B8A4bcF1e50e93D1Af" , "decimals" : 6 },
281
+ "amount0" : "1000000000000000000" , # 1 ETH
282
+ "amount1" : str (price * 1000000 ), # Price in USDC with 6 decimals
283
+ })
284
+
285
+ prices = evm_api ._extract_eth_prices (swaps )
286
+
287
+ # Outlier should be removed by IQR method
288
+ assert 10000 not in prices
289
+ assert len (prices ) == 5
290
+ assert all (3500 <= p <= 3504 for p in prices )
291
+
292
+ def test_extract_eth_prices_exception_handling (self ):
293
+ """Test _extract_eth_prices exception handling (lines 839-840)."""
294
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
295
+
296
+ swaps = [
297
+ # Valid swap
298
+ {
299
+ "token0" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" , "decimals" : 18 },
300
+ "token1" : {"address" : "0xA0b86a33E7c473D00e05A7B8A4bcF1e50e93D1Af" , "decimals" : 6 },
301
+ "amount0" : "1000000000000000000" ,
302
+ "amount1" : "3500000000" ,
303
+ },
304
+ # Swap that will cause ValueError
305
+ {
306
+ "token0" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" , "decimals" : 18 },
307
+ "token1" : {"address" : "0xA0b86a33E7c473D00e05A7B8A4bcF1e50e93D1Af" , "decimals" : 6 },
308
+ "amount0" : "invalid_number" ,
309
+ "amount1" : "3500000000" ,
310
+ },
311
+ # Swap missing required fields (KeyError)
312
+ {
313
+ "token0" : {"address" : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" },
314
+ # Missing token1
315
+ },
316
+ ]
317
+
318
+ prices = evm_api ._extract_eth_prices (swaps )
319
+
320
+ # Should only have the valid swap price
321
+ assert len (prices ) == 1
322
+ assert prices [0 ] == 3500.0
323
+
324
+ def test_is_eth_usdc_pair (self ):
325
+ """Test _is_eth_usdc_pair method (lines 854-856)."""
326
+ evm_api = EVMTokenAPI (NetworkId .MAINNET , "test_key" )
327
+
328
+ weth_addr = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
329
+ usdc_addr = "0xa0b86a33e7c473d00e05a7b8a4bcf1e50e93d1af"
330
+
331
+ # Valid pair
332
+ assert evm_api ._is_eth_usdc_pair (weth_addr , usdc_addr , weth_addr , usdc_addr ) is True
333
+ assert evm_api ._is_eth_usdc_pair (usdc_addr , weth_addr , weth_addr , usdc_addr ) is True
334
+
335
+ # Invalid pairs
336
+ assert evm_api ._is_eth_usdc_pair ("0xother" , usdc_addr , weth_addr , usdc_addr ) is False
337
+ assert evm_api ._is_eth_usdc_pair (weth_addr , "0xother" , weth_addr , usdc_addr ) is False
338
+ assert evm_api ._is_eth_usdc_pair ("0xother1" , "0xother2" , weth_addr , usdc_addr ) is False
0 commit comments