/ tests / test_convex.py
test_convex.py
  1  #!/usr/bin/env python3
  2  """
  3  Comprehensive tests for Convex Finance integration.
  4  
  5  Tests cover:
  6  - Vault creation and address lookup
  7  - Vault information queries
  8  - Deposits and withdrawals
  9  - Reward claiming
 10  - Error handling
 11  - cvxFXN staking
 12  
 13  Note: Test vault address (0x1234567890123456789012345678901234567890) is for
 14  fxUSD V2 Stability Pool (Earns FXN) - Pool ID 37. This is a TEST-ONLY address
 15  and should not be used in production.
 16  """
 17  
 18  import sys
 19  import os
 20  import unittest
 21  from unittest.mock import Mock, patch, MagicMock
 22  from decimal import Decimal
 23  from web3 import Web3
 24  
 25  # Add parent directory to path to use local development code
 26  # Must be first to override installed package
 27  local_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 28  if local_path not in sys.path:
 29      sys.path.insert(0, local_path)
 30  
 31  # Remove fx_sdk from cache if already imported (to force reload from local code)
 32  if 'fx_sdk' in sys.modules:
 33      del sys.modules['fx_sdk']
 34  if 'fx_sdk.client' in sys.modules:
 35      del sys.modules['fx_sdk.client']
 36  if 'fx_sdk.constants' in sys.modules:
 37      del sys.modules['fx_sdk.constants']
 38  if 'fx_sdk.exceptions' in sys.modules:
 39      del sys.modules['fx_sdk.exceptions']
 40  
 41  from fx_sdk import ProtocolClient, constants
 42  from fx_sdk.exceptions import (
 43      FXProtocolError,
 44      ContractCallError,
 45      InsufficientBalanceError
 46  )
 47  
 48  
 49  class TestConvexIntegration(unittest.TestCase):
 50      """Test suite for Convex Finance integration."""
 51      
 52      def setUp(self):
 53          """Set up test fixtures."""
 54          self.rpc_url = "https://eth.llamarpc.com"
 55          self.private_key = "0x" + "1" * 64  # Dummy private key for testing
 56          self.user_address = "0x742d35Cc6634C0532925a3b844Bc9e2385C6b0e0"
 57          # TEST-ONLY vault address for fxUSD V2 Stability Pool (Earns FXN) - Pool ID 37
 58          self.vault_address = "0x1234567890123456789012345678901234567890"
 59          self.pool_id = 37  # fxUSD V2 Stability Pool (Earns FXN)
 60          
 61          # Mock Web3 instance
 62          self.mock_w3 = Mock(spec=Web3)
 63          self.mock_w3.is_connected.return_value = True
 64          self.mock_w3.is_address.return_value = True
 65          self.mock_w3.eth = Mock()
 66          self.mock_w3.eth.contract = Mock()
 67          self.mock_w3.eth.get_transaction = Mock()
 68          self.mock_w3.eth.get_transaction_receipt = Mock()
 69          self.mock_w3.eth.wait_for_transaction_receipt = Mock()
 70      
 71      @patch('fx_sdk.client.Web3')
 72      def test_get_convex_vault_address_success(self, mock_web3_class):
 73          """Test successful vault address lookup."""
 74          mock_web3_class.return_value = self.mock_w3
 75          
 76          # Mock event logs - use MagicMock to support item assignment
 77          mock_event = MagicMock()
 78          mock_event.__getitem__ = Mock(side_effect=lambda k: {
 79              'transactionHash': b'\x00' * 32,
 80              'args': {
 81                  'user': self.user_address,
 82                  'poolid': self.pool_id
 83              }
 84          }.get(k))
 85          mock_event.transactionHash = b'\x00' * 32
 86          mock_event.args = Mock()
 87          mock_event.args.user = self.user_address
 88          mock_event.args.poolid = self.pool_id
 89          
 90          # Mock registry contract
 91          mock_registry = Mock()
 92          mock_registry.events.AddUserVault.get_logs.return_value = [mock_event]
 93          
 94          # Mock transaction receipt
 95          mock_receipt = {
 96              'blockNumber': 18000000,
 97              'logs': [
 98                  {
 99                      'address': self.vault_address,
100                      'topics': [],
101                      'data': ''
102                  }
103              ]
104          }
105          self.mock_w3.eth.get_transaction_receipt.return_value = mock_receipt
106          
107          # Mock vault contract
108          mock_vault = Mock()
109          mock_vault.functions.owner.return_value.call.return_value = self.user_address
110          mock_vault.functions.pid.return_value.call.return_value = self.pool_id
111          
112          client = ProtocolClient(self.rpc_url, private_key=self.private_key)
113          client._get_contract = Mock(side_effect=[mock_registry, mock_vault])
114          client.w3 = self.mock_w3
115          
116          # This will fail because get_convex_vault_address_from_tx is complex
117          # But we can test the basic flow
118          result = client.get_convex_vault_address(
119              self.user_address,
120              self.pool_id
121          )
122          
123          # Should attempt to query events
124          self.assertTrue(mock_registry.events.AddUserVault.get_logs.called)
125      
126      @patch('fx_sdk.client.Web3')
127      def test_get_convex_vault_address_not_found(self, mock_web3_class):
128          """Test vault address lookup when vault doesn't exist."""
129          mock_web3_class.return_value = self.mock_w3
130          
131          # Mock registry contract with no events
132          mock_registry = Mock()
133          mock_registry.events.AddUserVault.get_logs.return_value = []
134          
135          client = ProtocolClient(self.rpc_url, private_key=self.private_key)
136          client._get_contract = Mock(return_value=mock_registry)
137          
138          result = client.get_convex_vault_address(
139              self.user_address,
140              self.pool_id
141          )
142          
143          self.assertIsNone(result)
144      
145      @patch('fx_sdk.client.Web3')
146      def test_get_convex_vault_info_success(self, mock_web3_class):
147          """Test getting vault information."""
148          mock_web3_class.return_value = self.mock_w3
149          
150          # Mock vault contract
151          mock_vault = Mock()
152          mock_vault.functions.owner.return_value.call.return_value = self.user_address
153          mock_vault.functions.pid.return_value.call.return_value = self.pool_id
154          mock_vault.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
155          mock_vault.functions.gaugeAddress.return_value.call.return_value = "0x215D87bd3c7482E2348338815E059DE07Daf798A"
156          mock_vault.functions.rewards.return_value.call.return_value = "0x1234567890123456789012345678901234567890"
157          
158          client = ProtocolClient(self.rpc_url)
159          client._get_contract = Mock(return_value=mock_vault)
160          
161          info = client.get_convex_vault_info(self.vault_address)
162          
163          self.assertEqual(info['owner'], self.user_address)
164          self.assertEqual(info['pid'], self.pool_id)
165          self.assertEqual(info['staking_token'], constants.FXUSD_BASE_POOL)
166      
167      @patch('fx_sdk.client.Web3')
168      def test_get_convex_vault_info_invalid_address(self, mock_web3_class):
169          """Test getting vault info with invalid address."""
170          mock_web3_class.return_value = self.mock_w3
171          self.mock_w3.is_address.return_value = False
172          
173          client = ProtocolClient(self.rpc_url)
174          client.w3 = self.mock_w3
175          
176          with self.assertRaises(ContractCallError):
177              client.get_convex_vault_info("invalid_address")
178      
179      @patch('fx_sdk.client.Web3')
180      def test_get_convex_vault_balance_success(self, mock_web3_class):
181          """Test getting vault balance."""
182          mock_web3_class.return_value = self.mock_w3
183          
184          # Mock vault contract
185          mock_vault = Mock()
186          mock_vault.functions.owner.return_value.call.return_value = self.user_address
187          mock_vault.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
188          
189          # Mock token contract
190          mock_token = Mock()
191          mock_token.functions.balanceOf.return_value.call.return_value = 1000000000000000000  # 1 token
192          mock_token.functions.decimals.return_value.call.return_value = 18
193          
194          client = ProtocolClient(self.rpc_url)
195          client._get_contract = Mock(return_value=mock_vault)
196          client.w3 = self.mock_w3
197          client.w3.eth.contract = Mock(return_value=mock_token)
198          
199          balance = client.get_convex_vault_balance(self.vault_address)
200          
201          self.assertEqual(balance, Decimal("1"))
202      
203      @patch('fx_sdk.client.Web3')
204      def test_deposit_to_convex_vault_insufficient_balance(self, mock_web3_class):
205          """Test deposit with insufficient balance."""
206          mock_web3_class.return_value = self.mock_w3
207          
208          # Mock account
209          mock_account = Mock()
210          mock_account.address = self.user_address
211          
212          # Mock vault contract
213          mock_vault = Mock()
214          mock_vault.functions.owner.return_value.call.return_value = self.user_address
215          mock_vault.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
216          
217          # Mock token contract with insufficient balance
218          mock_token = Mock()
219          mock_token.functions.decimals.return_value.call.return_value = 18
220          mock_token.functions.balanceOf.return_value.call.return_value = 500000000000000000  # 0.5 tokens
221          mock_token.functions.allowance.return_value.call.return_value = 0
222          
223          client = ProtocolClient(self.rpc_url, private_key=self.private_key)
224          client.account = mock_account
225          client.address = self.user_address
226          client._get_contract = Mock(side_effect=[mock_vault, mock_token])
227          client._load_abi = Mock(return_value=[])
228          client.w3 = self.mock_w3
229          client.w3.eth.contract = Mock(return_value=mock_token)
230          
231          with self.assertRaises(InsufficientBalanceError):
232              client.deposit_to_convex_vault(self.vault_address, amount=1.0)
233      
234      @patch('fx_sdk.client.Web3')
235      def test_deposit_to_convex_vault_no_private_key(self, mock_web3_class):
236          """Test deposit without private key."""
237          mock_web3_class.return_value = self.mock_w3
238          
239          client = ProtocolClient(self.rpc_url)  # No private key
240          
241          with self.assertRaises(FXProtocolError):
242              client.deposit_to_convex_vault(self.vault_address, amount=1.0)
243      
244      @patch('fx_sdk.client.Web3')
245      def test_withdraw_from_convex_vault_insufficient_balance(self, mock_web3_class):
246          """Test withdrawal with insufficient vault balance."""
247          mock_web3_class.return_value = self.mock_w3
248          
249          # Mock account
250          mock_account = Mock()
251          mock_account.address = self.user_address
252          
253          # Mock vault contract
254          mock_vault = Mock()
255          mock_vault.functions.owner.return_value.call.return_value = self.user_address
256          mock_vault.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
257          
258          # Mock token contract
259          mock_token = Mock()
260          mock_token.functions.decimals.return_value.call.return_value = 18
261          
262          client = ProtocolClient(self.rpc_url, private_key=self.private_key)
263          client.account = mock_account
264          client.address = self.user_address
265          client._get_contract = Mock(side_effect=[mock_vault, mock_token])
266          client._load_abi = Mock(return_value=[])
267          client.w3 = self.mock_w3
268          client.w3.eth.contract = Mock(return_value=mock_token)
269          client.get_convex_vault_balance = Mock(return_value=Decimal("0.5"))
270          
271          with self.assertRaises(InsufficientBalanceError):
272              client.withdraw_from_convex_vault(self.vault_address, amount=1.0)
273      
274      @patch('fx_sdk.client.Web3')
275      def test_get_convex_vault_rewards_success(self, mock_web3_class):
276          """Test getting vault rewards."""
277          mock_web3_class.return_value = self.mock_w3
278          
279          # Mock vault contract
280          mock_vault = Mock()
281          mock_vault.functions.owner.return_value.call.return_value = self.user_address
282          mock_vault.functions.earned.return_value.call.return_value = (
283              ["0x365AccFCa291e7D3914637ABf1F7635dB165Bb09"],  # FXN token
284              [5000000000000000000]  # 5 tokens
285          )
286          
287          # Mock token contract for decimals
288          mock_token = Mock()
289          mock_token.functions.decimals.return_value.call.return_value = 18
290          
291          client = ProtocolClient(self.rpc_url)
292          client._get_contract = Mock(return_value=mock_vault)
293          client.w3 = self.mock_w3
294          client.w3.eth.contract = Mock(return_value=mock_token)
295          
296          rewards = client.get_convex_vault_rewards(self.vault_address)
297          
298          self.assertIn('token_addresses', rewards)
299          self.assertIn('amounts', rewards)
300          self.assertEqual(len(rewards['token_addresses']), 1)
301          self.assertEqual(
302              rewards['amounts']["0x365AccFCa291e7D3914637ABf1F7635dB165Bb09"],
303              Decimal("5")
304          )
305      
306      @patch('fx_sdk.client.Web3')
307      def test_claim_convex_vault_rewards_no_private_key(self, mock_web3_class):
308          """Test claiming rewards without private key."""
309          mock_web3_class.return_value = self.mock_w3
310          
311          client = ProtocolClient(self.rpc_url)  # No private key
312          
313          with self.assertRaises(FXProtocolError):
314              client.claim_convex_vault_rewards(self.vault_address)
315      
316      @patch('fx_sdk.client.Web3')
317      def test_get_cvxfxn_balance(self, mock_web3_class):
318          """Test getting cvxFXN balance."""
319          mock_web3_class.return_value = self.mock_w3
320          
321          client = ProtocolClient(self.rpc_url)
322          # Mock get_token_balance directly since it's called by get_cvxfxn_balance
323          client.get_token_balance = Mock(return_value=Decimal("2"))
324          
325          balance = client.get_cvxfxn_balance(self.user_address)
326          
327          self.assertEqual(balance, Decimal("2"))
328      
329      @patch('fx_sdk.client.Web3')
330      def test_get_staked_cvxfxn_balance(self, mock_web3_class):
331          """Test getting staked cvxFXN balance."""
332          mock_web3_class.return_value = self.mock_w3
333          
334          # Mock stake contract
335          mock_stake = Mock()
336          mock_stake.functions.balanceOf.return_value.call.return_value = 1000000000000000000  # 1 token
337          mock_stake.functions.decimals.return_value.call.return_value = 18
338          
339          client = ProtocolClient(self.rpc_url)
340          client._get_contract = Mock(return_value=mock_stake)
341          
342          balance = client.get_staked_cvxfxn_balance(self.user_address)
343          
344          self.assertEqual(balance, Decimal("1"))
345      
346      @patch('fx_sdk.client.Web3')
347      def test_get_cvxfxn_staking_rewards(self, mock_web3_class):
348          """Test getting cvxFXN staking rewards."""
349          mock_web3_class.return_value = self.mock_w3
350          
351          # Mock stake contract
352          mock_stake = Mock()
353          mock_stake.functions.earned.return_value.call.return_value = 500000000000000000  # 0.5 tokens
354          mock_stake.functions.decimals.return_value.call.return_value = 18
355          
356          client = ProtocolClient(self.rpc_url)
357          client._get_contract = Mock(return_value=mock_stake)
358          
359          rewards = client.get_cvxfxn_staking_rewards(self.user_address)
360          
361          self.assertEqual(rewards, Decimal("0.5"))
362      
363      @patch('fx_sdk.client.Web3')
364      def test_deposit_fxn_to_cvxfxn_no_private_key(self, mock_web3_class):
365          """Test depositing FXN to cvxFXN without private key."""
366          mock_web3_class.return_value = self.mock_w3
367          
368          client = ProtocolClient(self.rpc_url)  # No private key
369          
370          with self.assertRaises(FXProtocolError):
371              client.deposit_fxn_to_cvxfxn(amount=1.0)
372      
373      @patch('fx_sdk.client.Web3')
374      def test_stake_cvxfxn_no_private_key(self, mock_web3_class):
375          """Test staking cvxFXN without private key."""
376          mock_web3_class.return_value = self.mock_w3
377          
378          client = ProtocolClient(self.rpc_url)  # No private key
379          
380          with self.assertRaises(FXProtocolError):
381              client.stake_cvxfxn(amount=1.0)
382      
383      @patch('fx_sdk.client.Web3')
384      def test_get_convex_pool_info_by_id(self, mock_web3_class):
385          """Test getting pool info by pool ID."""
386          mock_web3_class.return_value = self.mock_w3
387          
388          client = ProtocolClient(self.rpc_url)
389          
390          pool_info = client.get_convex_pool_info(pool_id=37)
391          
392          self.assertEqual(pool_info['pool_id'], 37)
393          self.assertIn('name', pool_info)
394          self.assertIn('staked_token', pool_info)
395          self.assertEqual(pool_info['pool_key'], 'fxusd_stability_fxn')
396      
397      @patch('fx_sdk.client.Web3')
398      def test_get_convex_pool_info_by_key(self, mock_web3_class):
399          """Test getting pool info by pool key."""
400          mock_web3_class.return_value = self.mock_w3
401          
402          client = ProtocolClient(self.rpc_url)
403          
404          pool_info = client.get_convex_pool_info(pool_key='fxusd_stability_fxn')
405          
406          self.assertEqual(pool_info['pool_id'], 37)
407          self.assertEqual(pool_info['pool_key'], 'fxusd_stability_fxn')
408          self.assertIn('name', pool_info)
409      
410      @patch('fx_sdk.client.Web3')
411      def test_get_convex_pool_info_not_found(self, mock_web3_class):
412          """Test getting pool info for non-existent pool."""
413          mock_web3_class.return_value = self.mock_w3
414          
415          client = ProtocolClient(self.rpc_url)
416          
417          with self.assertRaises(FXProtocolError):
418              client.get_convex_pool_info(pool_id=99999)
419      
420      @patch('fx_sdk.client.Web3')
421      def test_get_all_convex_pools(self, mock_web3_class):
422          """Test getting all Convex pools."""
423          mock_web3_class.return_value = self.mock_w3
424          
425          client = ProtocolClient(self.rpc_url)
426          
427          all_pools = client.get_all_convex_pools()
428          
429          self.assertIsInstance(all_pools, dict)
430          self.assertGreater(len(all_pools), 0)
431          # Check that pool 37 is in the results
432          found_pool_37 = False
433          for pool_key, pool_info in all_pools.items():
434              if pool_info.get('pool_id') == 37:
435                  found_pool_37 = True
436                  self.assertEqual(pool_info['pool_key'], pool_key)
437                  break
438          self.assertTrue(found_pool_37, "Pool 37 should be in the results")
439      
440      @patch('fx_sdk.client.Web3')
441      def test_get_vault_balances_batch(self, mock_web3_class):
442          """Test batch query of vault balances."""
443          mock_web3_class.return_value = self.mock_w3
444          
445          client = ProtocolClient(self.rpc_url)
446          client.get_convex_vault_balance = Mock(side_effect=[
447              Decimal("100"),
448              Decimal("50"),
449              Decimal("0")
450          ])
451          
452          vault_addresses = [self.vault_address, "0x" + "1" * 40, "0x" + "2" * 40]
453          balances = client.get_vault_balances_batch(vault_addresses)
454          
455          self.assertEqual(len(balances), 3)
456          self.assertEqual(balances[self.vault_address], Decimal("100"))
457      
458      @patch('fx_sdk.client.Web3')
459      def test_get_vault_rewards_batch(self, mock_web3_class):
460          """Test batch query of vault rewards."""
461          mock_web3_class.return_value = self.mock_w3
462          
463          client = ProtocolClient(self.rpc_url)
464          client.get_convex_vault_rewards = Mock(return_value={
465              "token_addresses": ["0x365AccFCa291e7D3914637ABf1F7635dB165Bb09"],
466              "amounts": {"0x365AccFCa291e7D3914637ABf1F7635dB165Bb09": Decimal("5")}
467          })
468          
469          vault_addresses = [self.vault_address, "0x" + "1" * 40]
470          rewards = client.get_vault_rewards_batch(vault_addresses)
471          
472          self.assertEqual(len(rewards), 2)
473          self.assertIn(self.vault_address, rewards)
474          self.assertIn("token_addresses", rewards[self.vault_address])
475      
476      @patch('fx_sdk.client.Web3')
477      def test_get_convex_pool_apy(self, mock_web3_class):
478          """Test getting APY for a Convex pool."""
479          mock_web3_class.return_value = self.mock_w3
480          
481          # Mock booster contract
482          mock_booster = Mock()
483          mock_booster.functions.poolInfo.return_value.call.return_value = [
484              constants.FXUSD_BASE_POOL,  # lptoken
485              "0x1234567890123456789012345678901234567890",  # token
486              "0x215D87bd3c7482E2348338815E059DE07Daf798A",  # gauge
487              "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",  # crvRewards (BaseRewardPool)
488              "0x0000000000000000000000000000000000000000",  # stash
489              False  # shutdown
490          ]
491          
492          # Mock reward pool contract
493          mock_reward_pool = Mock()
494          mock_reward_pool.functions.rewardRate.return_value.call.return_value = 1000000000000000000  # 1 token per second
495          mock_reward_pool.functions.totalSupply.return_value.call.return_value = 100000000000000000000  # 100 tokens staked
496          mock_reward_pool.functions.periodFinish.return_value.call.return_value = 9999999999  # Far future
497          mock_reward_pool.functions.rewardToken.return_value.call.return_value = constants.FXN
498          mock_reward_pool.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
499          
500          # Mock token contracts
501          mock_reward_token = Mock()
502          mock_reward_token.functions.decimals.return_value.call.return_value = 18
503          mock_staking_token = Mock()
504          mock_staking_token.functions.decimals.return_value.call.return_value = 18
505          
506          # Mock block
507          mock_block = {'timestamp': 1000000000}
508          self.mock_w3.eth.get_block.return_value = mock_block
509          
510          client = ProtocolClient(self.rpc_url)
511          client._get_contract = Mock(side_effect=[
512              mock_booster,  # booster
513              mock_reward_pool,  # reward pool
514              mock_reward_token,  # reward token
515              mock_staking_token  # staking token
516          ])
517          client.w3 = self.mock_w3
518          
519          apy_data = client.get_convex_pool_apy(pool_id=37)
520          
521          self.assertIn('apy', apy_data)
522          self.assertIn('reward_rate', apy_data)
523          self.assertIn('total_staked', apy_data)
524          self.assertIn('is_active', apy_data)
525          self.assertEqual(apy_data['pool_id'], 37)
526          # APY should be approximately: (1 * 31536000) / 100 * 100 = 315360%
527          # But that's per second, so let's just check it's calculated
528          self.assertIsInstance(apy_data['apy'], (int, float))
529      
530      @patch('fx_sdk.client.Web3')
531      def test_get_convex_vault_apy(self, mock_web3_class):
532          """Test getting APY for a Convex vault."""
533          mock_web3_class.return_value = self.mock_w3
534          
535          # Mock vault contract
536          mock_vault = Mock()
537          mock_vault.functions.pid.return_value.call.return_value = 37
538          mock_vault.functions.rewards.return_value.call.return_value = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
539          mock_vault.functions.owner.return_value.call.return_value = self.user_address
540          
541          # Mock reward pool contract
542          mock_reward_pool = Mock()
543          mock_reward_pool.functions.rewardRate.return_value.call.return_value = 500000000000000000  # 0.5 token per second
544          mock_reward_pool.functions.totalSupply.return_value.call.return_value = 50000000000000000000  # 50 tokens staked
545          mock_reward_pool.functions.periodFinish.return_value.call.return_value = 9999999999
546          mock_reward_pool.functions.rewardToken.return_value.call.return_value = constants.FXN
547          mock_reward_pool.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
548          
549          # Mock token contracts
550          mock_reward_token = Mock()
551          mock_reward_token.functions.decimals.return_value.call.return_value = 18
552          mock_staking_token = Mock()
553          mock_staking_token.functions.decimals.return_value.call.return_value = 18
554          
555          # Mock block
556          mock_block = {'timestamp': 1000000000}
557          self.mock_w3.eth.get_block.return_value = mock_block
558          
559          client = ProtocolClient(self.rpc_url)
560          client._get_contract = Mock(side_effect=[
561              mock_vault,  # vault
562              mock_reward_pool,  # reward pool
563              mock_reward_token,  # reward token
564              mock_staking_token  # staking token
565          ])
566          client.w3 = self.mock_w3
567          client.get_convex_pool_info = Mock(return_value={"name": "Test Pool", "pool_key": "test"})
568          
569          apy_data = client.get_convex_vault_apy(self.vault_address)
570          
571          self.assertIn('apy', apy_data)
572          self.assertIn('pool_id', apy_data)
573          self.assertEqual(apy_data['pool_id'], 37)
574          self.assertIn('vault_address', apy_data)
575          self.assertEqual(apy_data['vault_address'], self.vault_address)
576      
577      @patch('fx_sdk.client.Web3')
578      def test_get_all_convex_pools_apy(self, mock_web3_class):
579          """Test getting APY for all Convex pools."""
580          mock_web3_class.return_value = self.mock_w3
581          
582          client = ProtocolClient(self.rpc_url)
583          client.get_convex_pool_apy = Mock(return_value={
584              "apy": 10.5,
585              "pool_id": 37,
586              "pool_name": "Test Pool",
587              "is_active": True
588          })
589          
590          apys = client.get_all_convex_pools_apy()
591          
592          self.assertIsInstance(apys, dict)
593          # Should have entries for pools that succeeded
594          # (Some may fail, which is expected)
595      
596      @patch('fx_sdk.client.Web3')
597      def test_get_convex_pool_details(self, mock_web3_class):
598          """Test getting comprehensive pool details."""
599          mock_web3_class.return_value = self.mock_w3
600          
601          # Mock booster contract
602          mock_booster = Mock()
603          mock_booster.functions.poolInfo.return_value.call.return_value = [
604              constants.FXUSD_BASE_POOL,  # lptoken
605              "0x1234567890123456789012345678901234567890",  # token
606              "0x215D87bd3c7482E2348338815E059DE07Daf798A",  # gauge
607              "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",  # crvRewards
608              "0x0000000000000000000000000000000000000000",  # stash
609              False  # shutdown
610          ]
611          
612          # Mock reward pool contract
613          mock_reward_pool = Mock()
614          mock_reward_pool.functions.totalSupply.return_value.call.return_value = 100000000000000000000
615          mock_reward_pool.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
616          mock_reward_pool.functions.rewardToken.return_value.call.return_value = constants.FXN
617          mock_reward_pool.functions.rewardRate.return_value.call.return_value = 1000000000000000000
618          mock_reward_pool.functions.periodFinish.return_value.call.return_value = 9999999999
619          
620          # Mock token contracts
621          mock_staking_token = Mock()
622          mock_staking_token.functions.decimals.return_value.call.return_value = 18
623          mock_reward_token = Mock()
624          mock_reward_token.functions.decimals.return_value.call.return_value = 18
625          
626          # Mock block
627          mock_block = {'timestamp': 1000000000}
628          self.mock_w3.eth.get_block.return_value = mock_block
629          
630          client = ProtocolClient(self.rpc_url)
631          client._get_contract = Mock(side_effect=[
632              mock_booster,  # booster
633              mock_reward_pool,  # reward pool (for TVL)
634              mock_staking_token,  # staking token
635              mock_reward_pool,  # reward pool (for rewards)
636              mock_reward_token  # reward token
637          ])
638          client.w3 = self.mock_w3
639          client.get_convex_pool_info = Mock(return_value={
640              "pool_id": 37,
641              "name": "fxUSD V2 Stability Pool (Earns FXN)",
642              "staked_token": constants.FXUSD_BASE_POOL
643          })
644          
645          details = client.get_convex_pool_details(pool_id=37)
646          
647          self.assertIn('pool_id', details)
648          self.assertIn('tvl', details)
649          self.assertIn('gauge_address', details)
650          self.assertIn('reward_tokens', details)
651          self.assertIn('rewards_active', details)
652          self.assertEqual(details['pool_id'], 37)
653      
654      @patch('fx_sdk.client.Web3')
655      def test_get_convex_pool_tvl(self, mock_web3_class):
656          """Test getting pool TVL."""
657          mock_web3_class.return_value = self.mock_w3
658          
659          # Mock booster and reward pool
660          mock_booster = Mock()
661          mock_booster.functions.poolInfo.return_value.call.return_value = [
662              constants.FXUSD_BASE_POOL,  # lptoken
663              "0x1234567890123456789012345678901234567890",  # token
664              "0x215D87bd3c7482E2348338815E059DE07Daf798A",  # gauge
665              "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",  # crvRewards
666              "0x0000000000000000000000000000000000000000",  # stash
667              False  # shutdown
668          ]
669          
670          mock_reward_pool = Mock()
671          mock_reward_pool.functions.totalSupply.return_value.call.return_value = 50000000000000000000
672          mock_reward_pool.functions.stakingToken.return_value.call.return_value = constants.FXUSD_BASE_POOL
673          
674          mock_staking_token = Mock()
675          mock_staking_token.functions.decimals.return_value.call.return_value = 18
676          
677          client = ProtocolClient(self.rpc_url)
678          client._get_contract = Mock(side_effect=[
679              mock_booster,
680              mock_reward_pool,
681              mock_staking_token
682          ])
683          client.w3 = self.mock_w3
684          
685          tvl = client.get_convex_pool_tvl(pool_id=37)
686          
687          self.assertIsNotNone(tvl)
688          self.assertIsInstance(tvl, Decimal)
689      
690      @patch('fx_sdk.client.Web3')
691      def test_get_convex_pool_reward_tokens(self, mock_web3_class):
692          """Test getting pool reward tokens."""
693          mock_web3_class.return_value = self.mock_w3
694          
695          mock_booster = Mock()
696          mock_booster.functions.poolInfo.return_value.call.return_value = [
697              constants.FXUSD_BASE_POOL,
698              "0x1234567890123456789012345678901234567890",
699              "0x215D87bd3c7482E2348338815E059DE07Daf798A",
700              "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
701              "0x0000000000000000000000000000000000000000",
702              False
703          ]
704          
705          mock_reward_pool = Mock()
706          mock_reward_pool.functions.rewardToken.return_value.call.return_value = constants.FXN
707          
708          client = ProtocolClient(self.rpc_url)
709          client._get_contract = Mock(side_effect=[mock_booster, mock_reward_pool])
710          client.w3 = self.mock_w3
711          
712          reward_tokens = client.get_convex_pool_reward_tokens(pool_id=37)
713          
714          self.assertIsInstance(reward_tokens, list)
715          self.assertEqual(len(reward_tokens), 1)
716          self.assertEqual(reward_tokens[0], constants.FXN)
717      
718      @patch('fx_sdk.client.Web3')
719      def test_get_convex_pool_gauge_address(self, mock_web3_class):
720          """Test getting pool gauge address."""
721          mock_web3_class.return_value = self.mock_w3
722          
723          mock_booster = Mock()
724          gauge_address = "0x215D87bd3c7482E2348338815E059DE07Daf798A"
725          mock_booster.functions.poolInfo.return_value.call.return_value = [
726              constants.FXUSD_BASE_POOL,
727              "0x1234567890123456789012345678901234567890",
728              gauge_address,
729              "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
730              "0x0000000000000000000000000000000000000000",
731              False
732          ]
733          
734          client = ProtocolClient(self.rpc_url)
735          client._get_contract = Mock(return_value=mock_booster)
736          client.w3 = self.mock_w3
737          
738          gauge = client.get_convex_pool_gauge_address(pool_id=37)
739          
740          self.assertIsNotNone(gauge)
741          self.assertEqual(gauge.lower(), gauge_address.lower())
742      
743      @patch('fx_sdk.client.Web3')
744      def test_get_all_convex_pools_tvl(self, mock_web3_class):
745          """Test getting TVL for all pools."""
746          mock_web3_class.return_value = self.mock_w3
747          
748          client = ProtocolClient(self.rpc_url)
749          client.get_convex_pool_tvl = Mock(return_value=Decimal("1000"))
750          
751          all_tvls = client.get_all_convex_pools_tvl()
752          
753          self.assertIsInstance(all_tvls, dict)
754          # Should have entries for all pools
755          self.assertGreater(len(all_tvls), 0)
756      
757      @patch('fx_sdk.client.Web3')
758      def test_get_convex_pool_statistics(self, mock_web3_class):
759          """Test getting comprehensive pool statistics."""
760          mock_web3_class.return_value = self.mock_w3
761          
762          client = ProtocolClient(self.rpc_url)
763          client.get_convex_pool_details = Mock(return_value={
764              "pool_id": 37,
765              "name": "Test Pool",
766              "tvl": 1000.0,
767              "reward_tokens": [constants.FXN],
768              "rewards_active": True
769          })
770          client.get_convex_pool_apy = Mock(return_value={
771              "apy": 10.5,
772              "reward_rate": 1.0,
773              "total_staked": 100.0
774          })
775          
776          stats = client.get_convex_pool_statistics(pool_id=37)
777          
778          self.assertIn('pool_id', stats)
779          self.assertIn('tvl', stats)
780          self.assertIn('apy', stats)
781          self.assertIn('statistics_available', stats)
782          self.assertTrue(stats['statistics_available'])
783  
784  
785  if __name__ == '__main__':
786      unittest.main()
787