iwa 0.0.33__py3-none-any.whl → 0.0.59__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. iwa/core/chain/interface.py +130 -11
  2. iwa/core/chain/models.py +15 -3
  3. iwa/core/chain/rate_limiter.py +48 -12
  4. iwa/core/chainlist.py +15 -10
  5. iwa/core/cli.py +4 -1
  6. iwa/core/contracts/cache.py +1 -1
  7. iwa/core/contracts/contract.py +1 -0
  8. iwa/core/contracts/decoder.py +10 -4
  9. iwa/core/http.py +31 -0
  10. iwa/core/ipfs.py +21 -7
  11. iwa/core/keys.py +65 -15
  12. iwa/core/models.py +58 -13
  13. iwa/core/pricing.py +10 -6
  14. iwa/core/rpc_monitor.py +1 -0
  15. iwa/core/secrets.py +27 -0
  16. iwa/core/services/account.py +1 -1
  17. iwa/core/services/balance.py +0 -23
  18. iwa/core/services/safe.py +72 -45
  19. iwa/core/services/safe_executor.py +350 -0
  20. iwa/core/services/transaction.py +43 -13
  21. iwa/core/services/transfer/erc20.py +14 -3
  22. iwa/core/services/transfer/native.py +14 -31
  23. iwa/core/services/transfer/swap.py +1 -0
  24. iwa/core/tests/test_gnosis_fee.py +91 -0
  25. iwa/core/tests/test_ipfs.py +85 -0
  26. iwa/core/tests/test_pricing.py +65 -0
  27. iwa/core/tests/test_regression_fixes.py +97 -0
  28. iwa/core/utils.py +2 -0
  29. iwa/core/wallet.py +6 -4
  30. iwa/plugins/gnosis/cow/quotes.py +2 -2
  31. iwa/plugins/gnosis/cow/swap.py +18 -32
  32. iwa/plugins/gnosis/tests/test_cow.py +19 -10
  33. iwa/plugins/olas/constants.py +15 -5
  34. iwa/plugins/olas/contracts/activity_checker.py +3 -3
  35. iwa/plugins/olas/contracts/staking.py +0 -1
  36. iwa/plugins/olas/events.py +15 -13
  37. iwa/plugins/olas/importer.py +29 -25
  38. iwa/plugins/olas/models.py +0 -3
  39. iwa/plugins/olas/plugin.py +16 -14
  40. iwa/plugins/olas/service_manager/drain.py +16 -9
  41. iwa/plugins/olas/service_manager/lifecycle.py +23 -12
  42. iwa/plugins/olas/service_manager/staking.py +15 -10
  43. iwa/plugins/olas/tests/test_importer_error_handling.py +13 -0
  44. iwa/plugins/olas/tests/test_olas_archiving.py +83 -0
  45. iwa/plugins/olas/tests/test_olas_integration.py +49 -29
  46. iwa/plugins/olas/tests/test_olas_view.py +5 -1
  47. iwa/plugins/olas/tests/test_service_manager.py +15 -17
  48. iwa/plugins/olas/tests/test_service_manager_errors.py +6 -5
  49. iwa/plugins/olas/tests/test_service_manager_flows.py +7 -6
  50. iwa/plugins/olas/tests/test_service_manager_rewards.py +5 -1
  51. iwa/plugins/olas/tests/test_service_staking.py +64 -38
  52. iwa/tools/drain_accounts.py +61 -0
  53. iwa/tools/list_contracts.py +2 -0
  54. iwa/tools/reset_env.py +2 -1
  55. iwa/tools/test_chainlist.py +5 -1
  56. iwa/tui/screens/wallets.py +2 -4
  57. iwa/web/routers/accounts.py +1 -1
  58. iwa/web/routers/olas/services.py +10 -5
  59. iwa/web/static/app.js +21 -9
  60. iwa/web/static/style.css +4 -0
  61. iwa/web/tests/test_web_endpoints.py +2 -2
  62. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/METADATA +6 -3
  63. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/RECORD +82 -71
  64. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/WHEEL +1 -1
  65. tests/test_balance_service.py +0 -43
  66. tests/test_chain.py +13 -5
  67. tests/test_cli.py +2 -2
  68. tests/test_drain_coverage.py +12 -6
  69. tests/test_keys.py +23 -23
  70. tests/test_rate_limiter.py +2 -2
  71. tests/test_rate_limiter_retry.py +103 -0
  72. tests/test_rpc_efficiency.py +4 -1
  73. tests/test_rpc_rate_limit.py +34 -0
  74. tests/test_rpc_rotation.py +59 -11
  75. tests/test_safe_coverage.py +37 -23
  76. tests/test_safe_executor.py +361 -0
  77. tests/test_safe_integration.py +153 -0
  78. tests/test_safe_service.py +1 -1
  79. tests/test_transfer_swap_unit.py +5 -1
  80. tests/test_pricing.py +0 -160
  81. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/entry_points.txt +0 -0
  82. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/licenses/LICENSE +0 -0
  83. {iwa-0.0.33.dist-info → iwa-0.0.59.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,85 @@
1
+ """Tests for IPFS module."""
2
+
3
+ from unittest.mock import ANY, MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ import iwa.core.ipfs as ipfs_module
8
+ from iwa.core.ipfs import push_to_ipfs_sync
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_config():
13
+ """Mock config."""
14
+ with patch("iwa.core.ipfs.Config") as mock_c:
15
+ mock_c.return_value.core.ipfs_api_url = "http://fake-ipfs:5001"
16
+ yield mock_c
17
+
18
+
19
+ @pytest.fixture
20
+ def mock_cid_decode():
21
+ """Mock CID.decode."""
22
+ with patch("iwa.core.ipfs.CID") as mock_cid:
23
+ # returns an object that has version, codec, hashfun.name, raw_digest attributes
24
+ mock_decoded = MagicMock()
25
+ mock_decoded.version = 1
26
+ mock_decoded.codec = "raw"
27
+ mock_decoded.hashfun.name = "sha2-256"
28
+ mock_decoded.raw_digest = b"digest"
29
+
30
+ mock_cid.decode.return_value = mock_decoded
31
+ yield mock_cid
32
+
33
+
34
+ def test_push_to_ipfs_sync_uses_session(mock_config, mock_cid_decode):
35
+ """Test push_to_ipfs_sync uses persistent session."""
36
+ # Reset global session for test
37
+ ipfs_module._SYNC_SESSION = None
38
+
39
+ # Mock requests.Session
40
+ with patch("requests.Session") as mock_session_cls:
41
+ mock_session = MagicMock()
42
+ mock_session_cls.return_value = mock_session
43
+
44
+ # Mock response
45
+ mock_response = MagicMock()
46
+ mock_response.status_code = 200
47
+ mock_response.json.return_value = {"Hash": "QmTestHash"}
48
+ mock_session.post.return_value = mock_response
49
+
50
+ # Call function
51
+ cid_str, cid_hex = push_to_ipfs_sync(b"test data")
52
+
53
+ # Verify Session was created
54
+ mock_session_cls.assert_called_once()
55
+
56
+ # Verify post called on session
57
+ mock_session.post.assert_called_once()
58
+
59
+ # Verify session is stored globally
60
+ assert ipfs_module._SYNC_SESSION == mock_session
61
+
62
+ # Verify second call reuses session
63
+ push_to_ipfs_sync(b"test data 2")
64
+ mock_session_cls.assert_called_once() # Should still be 1 call
65
+ assert mock_session.post.call_count == 2
66
+
67
+
68
+ def test_push_to_ipfs_sync_retry_config(mock_cid_decode):
69
+ """Test session matches retry config."""
70
+ # Reset
71
+ ipfs_module._SYNC_SESSION = None
72
+
73
+ with patch("requests.Session") as mock_session_cls:
74
+ mock_session = MagicMock()
75
+ mock_session_cls.return_value = mock_session
76
+ mock_session.post.return_value.status_code = 200
77
+ mock_session.post.return_value.json.return_value = {"Hash": "QmHash"}
78
+
79
+ push_to_ipfs_sync(b"data")
80
+
81
+ # Verify adapter mounting
82
+ # Since we mock the class return value, we check calls on the return value
83
+ assert mock_session.mount.call_count == 2
84
+ mock_session.mount.assert_any_call("https://", ANY)
85
+ mock_session.mount.assert_any_call("http://", ANY)
@@ -0,0 +1,65 @@
1
+ """Tests for Pricing module."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+ import requests
7
+
8
+ from iwa.core.pricing import PriceService
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_secrets():
13
+ """Mock secrets."""
14
+ with patch("iwa.core.pricing.secrets") as mock_s:
15
+ mock_s.coingecko_api_key.get_secret_value.return_value = "fake_key"
16
+ yield mock_s
17
+
18
+
19
+ @pytest.fixture
20
+ def price_service(mock_secrets):
21
+ """PriceService fixture."""
22
+ return PriceService()
23
+
24
+
25
+ def test_init_session(price_service):
26
+ """Test session initialization."""
27
+ assert isinstance(price_service.session, requests.Session)
28
+
29
+ # Verify adapters are mounted
30
+ assert "https://" in price_service.session.adapters
31
+ assert "http://" in price_service.session.adapters
32
+
33
+ # Verify retry configuration in adapter
34
+ adapter = price_service.session.adapters["https://"]
35
+ assert adapter.max_retries.total == 3
36
+ assert adapter.max_retries.status_forcelist == [429, 500, 502, 503, 504]
37
+
38
+
39
+ def test_close(price_service):
40
+ """Test close method."""
41
+ price_service.session = MagicMock()
42
+ price_service.close()
43
+ price_service.session.close.assert_called_once()
44
+
45
+
46
+ def test_get_token_price_uses_session(price_service):
47
+ """Test get_token_price uses session."""
48
+ price_service.session = MagicMock()
49
+
50
+ # Mock response
51
+ mock_response = MagicMock()
52
+ mock_response.status_code = 200
53
+ mock_response.json.return_value = {"autonolas": {"eur": 5.0}}
54
+ price_service.session.get.return_value = mock_response
55
+
56
+ price = price_service.get_token_price("autonolas", "eur")
57
+
58
+ assert price == 5.0
59
+ price_service.session.get.assert_called()
60
+
61
+ # Verify call args
62
+ args, kwargs = price_service.session.get.call_args
63
+ assert "api.coingecko.com" in args[0]
64
+ assert kwargs["params"]["ids"] == "autonolas"
65
+ assert kwargs["params"]["vs_currencies"] == "eur"
@@ -0,0 +1,97 @@
1
+ """Regression tests for recent fixes."""
2
+
3
+ import unittest
4
+ from pathlib import Path
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ from iwa.core.keys import KeyStorage
8
+ from iwa.core.models import EthereumAddress
9
+ from iwa.core.services.transaction import TransactionService
10
+
11
+
12
+ class TestRegressionFixes(unittest.TestCase):
13
+ """Test regression fixes for fees and serialization."""
14
+
15
+ def test_transaction_service_fee_autofill(self):
16
+ """Test that TransactionService auto-fills fees if missing."""
17
+ # 1. Setup mocks
18
+ mock_key_storage = MagicMock()
19
+ mock_account_service = MagicMock()
20
+
21
+ # Mock account resolution
22
+ mock_account = MagicMock()
23
+ mock_account.address = "0x1234567890123456789012345678901234567890"
24
+ mock_account.tag = "signer"
25
+ mock_account_service.resolve_account.return_value = mock_account
26
+
27
+ service = TransactionService(mock_key_storage, mock_account_service)
28
+
29
+ # 2. Mock ChainInterface to return specific fees
30
+ mock_chain_interface = MagicMock()
31
+ mock_chain_interface.chain.chain_id = 100
32
+ mock_chain_interface.web3.eth.get_transaction_count.return_value = 1
33
+
34
+ # This is what we are testing: get_suggested_fees() provides the safety net
35
+ mock_chain_interface.get_suggested_fees.return_value = {
36
+ "maxFeePerGas": 1500,
37
+ "maxPriorityFeePerGas": 10,
38
+ }
39
+
40
+ with patch("iwa.core.services.transaction.ChainInterfaces") as mock_interfaces:
41
+ mock_interfaces.return_value.get.return_value = mock_chain_interface
42
+
43
+ # 3. Prepare transaction WITHOUT fees
44
+ tx = {"to": "0x09312C66A14a024B4e903D986Ca7E2C0dDD06227", "value": 1000, "gas": 21000}
45
+
46
+ # 4. Run internal preparation
47
+ service._prepare_transaction(tx, "signer", mock_chain_interface)
48
+
49
+ # 5. Verify fees were auto-filled
50
+ self.assertEqual(tx["maxFeePerGas"], 1500)
51
+ self.assertEqual(tx["maxPriorityFeePerGas"], 10)
52
+ self.assertIn("nonce", tx)
53
+ self.assertEqual(tx["chainId"], 100)
54
+
55
+ def test_key_storage_mode_json_serialization(self):
56
+ """Test that KeyStorage uses mode='json' to serialize EthereumAddress correctly."""
57
+ import tempfile
58
+
59
+ from iwa.core.models import StoredAccount
60
+
61
+ with tempfile.TemporaryDirectory() as tmp_dir:
62
+ tmp_path = Path(tmp_dir) / "wallet.json"
63
+
64
+ # 1. Initialize KeyStorage
65
+ storage = KeyStorage(path=tmp_path, password="test")
66
+
67
+ # 2. Add an account with a real EthereumAddress object
68
+ addr_str = "0x1234567890123456789012345678901234567890"
69
+ addr_obj = EthereumAddress(addr_str)
70
+ acc = StoredAccount(address=addr_obj, tag="test-tag")
71
+ storage.accounts = {addr_obj: acc}
72
+
73
+ # 3. Mock json.dump to see what's being written
74
+ with patch("json.dump") as mock_dump:
75
+ # We need to mock open() as well to prevent real file creation if not needed,
76
+ # but since we are in a temp dir, it's fine.
77
+ storage.save()
78
+
79
+ # 4. Capture the data passed to json.dump
80
+ self.assertTrue(mock_dump.called)
81
+ dumped_data = mock_dump.call_args[0][0]
82
+
83
+ # 5. Verify the address is a plain string in the dump
84
+ accounts = dumped_data["accounts"]
85
+
86
+ for key in accounts.keys():
87
+ # Key serialization in Pydantic v2 model_dump(mode='json')
88
+ self.assertIsInstance(key, str, f"Key {key} should be a string")
89
+ self.assertEqual(key.lower(), addr_str.lower())
90
+
91
+ # Also check the address field inside the value
92
+ self.assertIsInstance(accounts[key]["address"], str)
93
+ self.assertEqual(accounts[key]["address"].lower(), addr_str.lower())
94
+
95
+
96
+ if __name__ == "__main__":
97
+ unittest.main()
iwa/core/utils.py CHANGED
@@ -85,9 +85,11 @@ def configure_logger():
85
85
  configure_logger.configured = True
86
86
  return logger
87
87
 
88
+
88
89
  def get_version(package_name: str) -> str:
89
90
  """Get package version."""
90
91
  from importlib.metadata import PackageNotFoundError, version
92
+
91
93
  try:
92
94
  return version(package_name)
93
95
  except PackageNotFoundError:
iwa/core/wallet.py CHANGED
@@ -1,13 +1,13 @@
1
1
  """Wallet module."""
2
2
 
3
3
  from concurrent.futures import ThreadPoolExecutor
4
- from typing import List, Optional, Tuple
4
+ from typing import List, Optional, Tuple, Union
5
5
 
6
6
  from web3.types import Wei
7
7
 
8
8
  from iwa.core.chain import SupportedChain
9
9
  from iwa.core.db import init_db
10
- from iwa.core.keys import KeyStorage
10
+ from iwa.core.keys import EncryptedAccount, KeyStorage
11
11
  from iwa.core.models import EthereumAddress, StoredSafeAccount
12
12
  from iwa.core.services import (
13
13
  AccountService,
@@ -37,7 +37,9 @@ class Wallet:
37
37
  self.balance_service = BalanceService(self.key_storage, self.account_service)
38
38
  self.safe_service = SafeService(self.key_storage, self.account_service)
39
39
  # self.transaction_manager = TransactionManager(self.key_storage, self.account_service)
40
- self.transaction_service = TransactionService(self.key_storage, self.account_service, self.safe_service)
40
+ self.transaction_service = TransactionService(
41
+ self.key_storage, self.account_service, self.safe_service
42
+ )
41
43
 
42
44
  self.transfer_service = TransferService(
43
45
  self.key_storage,
@@ -51,7 +53,7 @@ class Wallet:
51
53
  init_db()
52
54
 
53
55
  @property
54
- def master_account(self) -> Optional[StoredSafeAccount]:
56
+ def master_account(self) -> Optional[Union[EncryptedAccount, StoredSafeAccount]]:
55
57
  """Get master account"""
56
58
  return self.account_service.master_account
57
59
 
@@ -33,7 +33,7 @@ async def get_max_sell_amount_wei(
33
33
  safe_address: ChecksumAddress | None = None,
34
34
  app_data: str | None = None,
35
35
  env: "Envs" = "prod",
36
- slippage_tolerance: float = 0.005,
36
+ slippage_tolerance: float = 0.015,
37
37
  ) -> int:
38
38
  """Calculate the estimated sell amount needed to buy a fixed amount of tokens."""
39
39
  if app_data is None:
@@ -95,7 +95,7 @@ async def get_max_buy_amount_wei(
95
95
  safe_address: ChecksumAddress | None = None,
96
96
  app_data: str | None = None,
97
97
  env: "Envs" = "prod",
98
- slippage_tolerance: float = 0.005,
98
+ slippage_tolerance: float = 0.015,
99
99
  ) -> int:
100
100
  """Calculate the maximum buy amount for a given sell amount."""
101
101
  if app_data is None:
@@ -96,13 +96,9 @@ class CowSwap:
96
96
 
97
97
  logger.info(f"Checking order status for UID: {order.uid}")
98
98
 
99
- max_retries = 6
100
- sleep_between_retries = 25
101
- retries = 0
102
-
103
- while retries < max_retries:
104
- retries += 1
99
+ sleep_between_retries = 15
105
100
 
101
+ while True:
106
102
  try:
107
103
  # Use a thread executor for blocking requests.get
108
104
  loop = asyncio.get_event_loop()
@@ -116,47 +112,37 @@ class CowSwap:
116
112
 
117
113
  if response.status_code != HTTP_OK:
118
114
  logger.debug(
119
- f"Order status check {retries}/{max_retries}: HTTP {response.status_code}. Retry in {sleep_between_retries}s"
115
+ f"Order status check: HTTP {response.status_code}. Retry in {sleep_between_retries}s"
120
116
  )
121
117
  await asyncio.sleep(sleep_between_retries)
122
118
  continue
123
119
 
124
120
  order_data = response.json()
125
-
126
121
  status = order_data.get("status", "unknown")
127
122
  valid_to = int(order_data.get("validTo", 0))
128
123
  current_time = int(time.time())
129
124
 
130
- if status == "expired" or (valid_to > 0 and current_time > valid_to):
131
- logger.error(
132
- f"Order expired without execution (Status: {status}, ValidTo: {valid_to}, Now: {current_time})."
133
- )
134
- return None
135
-
125
+ # Log current status
136
126
  executed_sell = int(order_data.get("executedSellAmount", "0"))
137
127
  executed_buy = int(order_data.get("executedBuyAmount", "0"))
138
128
 
139
- if executed_sell > 0 or executed_buy > 0:
129
+ if executed_sell > 0 or executed_buy > 0 or status == "fulfilled":
140
130
  logger.info("Order executed successfully.")
141
- sell_price = order_data.get("quote", {}).get("sellTokenPrice", None)
142
- buy_price = order_data.get("quote", {}).get("buyTokenPrice", None)
143
-
144
- if sell_price is not None:
145
- logger.debug(f"Sell price: ${float(sell_price):.2f}")
131
+ return order_data
146
132
 
147
- if buy_price is not None:
148
- logger.debug(f"Buy price: ${float(buy_price):.2f}")
133
+ if status in ["expired", "cancelled"]:
134
+ logger.error(f"Order {status} without execution.")
135
+ return None
149
136
 
150
- return order_data
137
+ if valid_to > 0 and current_time > valid_to + 60:
138
+ logger.error(
139
+ f"Order timeout: current time {current_time} exceeded valid_to {valid_to} by >60s."
140
+ )
141
+ return None
151
142
 
152
- logger.info(
153
- f"Order pending... ({retries}/{max_retries}). Retry in {sleep_between_retries}s"
154
- )
143
+ logger.info(f"Order status: {status}. Waiting {sleep_between_retries}s...")
155
144
  await asyncio.sleep(sleep_between_retries)
156
145
 
157
- logger.warning("Max retries reached. Order status unknown.")
158
- return None
159
-
160
146
  async def swap(
161
147
  self,
162
148
  amount_wei: Wei,
@@ -255,7 +241,7 @@ class CowSwap:
255
241
  safe_address: ChecksumAddress | None = None,
256
242
  app_data: str | None = None,
257
243
  env: "Envs" = "prod",
258
- slippage_tolerance: float = 0.005,
244
+ slippage_tolerance: float = 0.015,
259
245
  ) -> int:
260
246
  """Calculate the estimated sell amount needed to buy a fixed amount of tokens."""
261
247
  return await get_max_sell_amount_wei(
@@ -278,7 +264,7 @@ class CowSwap:
278
264
  safe_address: ChecksumAddress | None = None,
279
265
  app_data: str | None = None,
280
266
  env: "Envs" = "prod",
281
- slippage_tolerance: float = 0.005,
267
+ slippage_tolerance: float = 0.015,
282
268
  ) -> int:
283
269
  """Calculate the maximum buy amount for a given sell amount."""
284
270
  return await get_max_buy_amount_wei(
@@ -304,7 +290,7 @@ class CowSwap:
304
290
  app_data: str | None = None,
305
291
  valid_to: int | None = None,
306
292
  env: "Envs" = "prod",
307
- slippage_tolerance: float = 0.005,
293
+ slippage_tolerance: float = 0.015,
308
294
  partially_fillable: bool = False,
309
295
  ) -> "CompletedOrder":
310
296
  """Execute a 'Buy' order (Exact Output) on CoW Protocol."""
@@ -89,8 +89,8 @@ def test_init(cowswap, mock_chain):
89
89
  async def test_get_max_sell_amount_wei(cowswap, mock_cowpy_modules):
90
90
  """Test get_max_sell_amount_wei."""
91
91
  amount = await cowswap.get_max_sell_amount_wei(100, "0xSell", "0xBuy")
92
- # mocked sellAmount root is "100", slippage is 0.005 default -> 100 * 1.005 = 100
93
- assert amount == 100
92
+ # mocked sellAmount root is "100", slippage is 0.015 default -> 100 * 1.015 = 101.5 -> int 101
93
+ assert amount == 101
94
94
  mock_cowpy_modules["get_order_quote"].assert_called_once()
95
95
 
96
96
 
@@ -98,8 +98,8 @@ async def test_get_max_sell_amount_wei(cowswap, mock_cowpy_modules):
98
98
  async def test_get_max_buy_amount_wei(cowswap, mock_cowpy_modules):
99
99
  """Test get_max_buy_amount_wei."""
100
100
  amount = await cowswap.get_max_buy_amount_wei(100, "0xSell", "0xBuy")
101
- # mocked buyAmount root is "90", slippage 0.005 -> 90 * 0.995 = 89.55 -> int 89
102
- assert amount == 89
101
+ # mocked buyAmount root is "90", slippage 0.015 -> 90 * 0.985 = 88.65 -> int 88
102
+ assert amount == 88
103
103
  mock_cowpy_modules["get_order_quote"].assert_called_once()
104
104
 
105
105
 
@@ -213,15 +213,24 @@ async def test_check_cowswap_order_expired(cowswap):
213
213
 
214
214
  @pytest.mark.asyncio
215
215
  async def test_check_cowswap_order_timeout(cowswap):
216
- """Test check_cowswap_order timeout."""
216
+ """Test check_cowswap_order timeout after exceeding valid_to."""
217
217
  mock_order = MagicMock()
218
218
  mock_order.url = "http://api/order"
219
219
 
220
220
  with patch("requests.get") as mock_get:
221
221
  mock_get.return_value.status_code = 200
222
- mock_get.return_value.json.return_value = {"status": "open", "executedSellAmount": "0"}
222
+ # Order is always open
223
+ mock_get.return_value.json.return_value = {
224
+ "status": "open",
225
+ "executedSellAmount": "0",
226
+ "validTo": 1000,
227
+ }
223
228
 
224
- # Speed up retry sleep (asyncio.sleep)
225
- with patch("asyncio.sleep", new_callable=AsyncMock):
226
- result = await cowswap.check_cowswap_order(mock_order)
227
- assert result is None
229
+ # Mock time to start at 900 and then jump to 1100 to trigger timeout
230
+ with patch("time.time") as mock_time:
231
+ mock_time.side_effect = [900, 1100]
232
+ # Speed up retry sleep (asyncio.sleep)
233
+ with patch("asyncio.sleep", new_callable=AsyncMock):
234
+ result = await cowswap.check_cowswap_order(mock_order)
235
+ assert result is None
236
+ assert mock_time.call_count >= 2
@@ -92,8 +92,12 @@ OLAS_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
92
92
  OLAS_TRADER_STAKING_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
93
93
  "gnosis": {
94
94
  # === LEGACY (no marketplace) ===
95
- "Hobbyist 1 Legacy (100 OLAS)": EthereumAddress("0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C"),
96
- "Hobbyist 2 Legacy (500 OLAS)": EthereumAddress("0x238EB6993b90A978ec6AAD7530D6429c949C08DA"),
95
+ "Hobbyist 1 Legacy (100 OLAS)": EthereumAddress(
96
+ "0x389B46C259631Acd6a69Bde8B6cEe218230bAE8C"
97
+ ),
98
+ "Hobbyist 2 Legacy (500 OLAS)": EthereumAddress(
99
+ "0x238EB6993b90A978ec6AAD7530D6429c949C08DA"
100
+ ),
97
101
  "Expert Legacy (1k OLAS)": EthereumAddress("0x5344B7DD311e5d3DdDd46A4f71481Bd7b05AAA3e"),
98
102
  "Expert 2 Legacy (1k OLAS)": EthereumAddress("0xb964e44c126410df341ae04B13aB10A985fE3513"),
99
103
  "Expert 3 Legacy (2k OLAS)": EthereumAddress("0x80faD33Cadb5F53f9D29F02Db97D682E8B101618"),
@@ -103,9 +107,15 @@ OLAS_TRADER_STAKING_CONTRACTS: Dict[str, Dict[str, EthereumAddress]] = {
103
107
  "Expert 7 Legacy (10k OLAS)": EthereumAddress("0xD7A3C8b975f71030135f1a66E9e23164d54fF455"),
104
108
  "Expert 8 Legacy (2k OLAS)": EthereumAddress("0x356C108D49C5eebd21c84c04E9162de41933030c"),
105
109
  "Expert 9 Legacy (10k OLAS)": EthereumAddress("0x17dBAe44BC5618Cc254055B386A29576b4F87015"),
106
- "Expert 10 Legacy (10k OLAS)": EthereumAddress("0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f"),
107
- "Expert 11 Legacy (10k OLAS)": EthereumAddress("0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7"),
108
- "Expert 12 Legacy (10k OLAS)": EthereumAddress("0xF4a75F476801B3fBB2e7093aCDcc3576593Cc1fc"),
110
+ "Expert 10 Legacy (10k OLAS)": EthereumAddress(
111
+ "0xB0ef657b8302bd2c74B6E6D9B2b4b39145b19c6f"
112
+ ),
113
+ "Expert 11 Legacy (10k OLAS)": EthereumAddress(
114
+ "0x3112c1613eAC3dBAE3D4E38CeF023eb9E2C91CF7"
115
+ ),
116
+ "Expert 12 Legacy (10k OLAS)": EthereumAddress(
117
+ "0xF4a75F476801B3fBB2e7093aCDcc3576593Cc1fc"
118
+ ),
109
119
  # === MM v1 (old marketplace 0x4554fE75...) ===
110
120
  "Expert 15 MM v1 (10k OLAS)": EthereumAddress("0x88eB38FF79fBa8C19943C0e5Acfa67D5876AdCC1"),
111
121
  "Expert 16 MM v1 (10k OLAS)": EthereumAddress("0x6c65430515c70a3f5E62107CC301685B7D46f991"),
@@ -48,7 +48,6 @@ class ActivityCheckerContract(ContractInstance):
48
48
  self._agent_mech: Optional[EthereumAddress] = None
49
49
  self._liveness_ratio: Optional[int] = None
50
50
 
51
-
52
51
  def get_multisig_nonces(self, multisig: EthereumAddress) -> Tuple[int, int]:
53
52
  """Get the nonces for a multisig address.
54
53
 
@@ -64,7 +63,6 @@ class ActivityCheckerContract(ContractInstance):
64
63
  nonces = self.contract.functions.getMultisigNonces(multisig).call()
65
64
  return (nonces[0], nonces[1])
66
65
 
67
-
68
66
  @property
69
67
  def mech_marketplace(self) -> Optional[EthereumAddress]:
70
68
  """Get the mech marketplace address."""
@@ -83,7 +81,9 @@ class ActivityCheckerContract(ContractInstance):
83
81
  try:
84
82
  agent_mech_function = getattr(self.contract.functions, "agentMech", None)
85
83
  self._agent_mech = (
86
- agent_mech_function().call() if agent_mech_function else DEFAULT_MECH_CONTRACT_ADDRESS
84
+ agent_mech_function().call()
85
+ if agent_mech_function
86
+ else DEFAULT_MECH_CONTRACT_ADDRESS
87
87
  )
88
88
  except Exception:
89
89
  self._agent_mech = DEFAULT_MECH_CONTRACT_ADDRESS
@@ -82,7 +82,6 @@ class StakingContract(ContractInstance):
82
82
  self._activity_checker: Optional[ActivityCheckerContract] = None
83
83
  self._activity_checker_address: Optional[EthereumAddress] = None
84
84
 
85
-
86
85
  def get_requirements(self) -> Dict[str, Union[str, int]]:
87
86
  """Get the contract requirements for token and deposits.
88
87
 
@@ -1,6 +1,5 @@
1
1
  """Event-based cache invalidation for Olas contracts."""
2
2
 
3
-
4
3
  from loguru import logger
5
4
 
6
5
  from iwa.core.contracts.cache import ContractCache
@@ -75,7 +74,7 @@ class OlasEventInvalidator:
75
74
  except Exception as e:
76
75
  logger.error(f"Error in OlasEventInvalidator: {e}")
77
76
 
78
- time.sleep(10) # check every 10 seconds
77
+ time.sleep(10) # check every 10 seconds
79
78
 
80
79
  def _check_events(self, from_block: int, to_block: int):
81
80
  """Check for relevant events in the block range."""
@@ -101,14 +100,19 @@ class OlasEventInvalidator:
101
100
  StakingContract, self.staking_addresses[0], self.chain_name
102
101
  )
103
102
 
104
-
105
- logs = self.web3.eth.get_logs({
106
- "fromBlock": from_block,
107
- "toBlock": to_block,
108
- "address": self.staking_addresses,
109
- "topics": [self.web3.keccak(text="Checkpoint(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)").hex()]
110
- # Note: signature might vary, safer to use the event object if ABI allows
111
- })
103
+ logs = self.web3.eth.get_logs(
104
+ {
105
+ "fromBlock": from_block,
106
+ "toBlock": to_block,
107
+ "address": self.staking_addresses,
108
+ "topics": [
109
+ self.web3.keccak(
110
+ text="Checkpoint(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)"
111
+ ).hex()
112
+ ],
113
+ # Note: signature might vary, safer to use the event object if ABI allows
114
+ }
115
+ )
112
116
 
113
117
  # If we used the contract event object to filter, it handles the topic generation:
114
118
  # logs = checkpoint_event_abi.get_logs(fromBlock=from_block, toBlock=to_block)
@@ -130,9 +134,7 @@ class OlasEventInvalidator:
130
134
  # self.contract_cache.invalidate(StakingContract, addr, self.chain_name)
131
135
 
132
136
  # Option B: Get instance and clear specific cache (safe public access)
133
- instance = self.contract_cache.get_if_cached(
134
- StakingContract, addr, self.chain_name
135
- )
137
+ instance = self.contract_cache.get_if_cached(StakingContract, addr, self.chain_name)
136
138
  if instance:
137
139
  instance.clear_epoch_cache()
138
140
  logger.debug(f"Cleared epoch cache for {addr}")