iwa 0.0.0__py3-none-any.whl → 0.0.1a2__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 (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,144 @@
1
+ """Tests for Olas models and OlasConfig."""
2
+
3
+ from iwa.plugins.olas.models import OlasConfig, Service, StakingStatus
4
+
5
+
6
+ class TestOlasConfig:
7
+ """Tests for OlasConfig class."""
8
+
9
+ def test_add_service(self):
10
+ """Test add_service adds service to dict."""
11
+ config = OlasConfig()
12
+ service = Service(
13
+ service_name="test",
14
+ chain_name="gnosis",
15
+ service_id=456,
16
+ agent_ids=[25],
17
+ service_owner_address="0x1234567890123456789012345678901234567890",
18
+ )
19
+
20
+ config.add_service(service)
21
+
22
+ assert "gnosis:456" in config.services
23
+ assert config.services["gnosis:456"] == service
24
+
25
+ def test_remove_service_success(self):
26
+ """Test remove_service removes existing service."""
27
+ config = OlasConfig()
28
+ service = Service(
29
+ service_name="test",
30
+ chain_name="gnosis",
31
+ service_id=789,
32
+ agent_ids=[25],
33
+ service_owner_address="0x1234567890123456789012345678901234567890",
34
+ )
35
+ config.services["gnosis:789"] = service
36
+
37
+ result = config.remove_service("gnosis:789")
38
+
39
+ assert result is True
40
+ assert "gnosis:789" not in config.services
41
+
42
+ def test_remove_service_not_found(self):
43
+ """Test remove_service returns False when not found."""
44
+ config = OlasConfig()
45
+ result = config.remove_service("gnosis:999")
46
+ assert result is False
47
+
48
+ def test_get_service(self):
49
+ """Test get_service by chain and id."""
50
+ config = OlasConfig()
51
+ service = Service(
52
+ service_name="test",
53
+ chain_name="ethereum",
54
+ service_id=200,
55
+ agent_ids=[25],
56
+ service_owner_address="0x1234567890123456789012345678901234567890",
57
+ )
58
+ config.services["ethereum:200"] = service
59
+
60
+ result = config.get_service("ethereum", 200)
61
+ assert result is not None
62
+ assert result.service_id == 200
63
+
64
+ def test_get_service_not_found(self):
65
+ """Test get_service returns None when not found."""
66
+ config = OlasConfig()
67
+ result = config.get_service("base", 999)
68
+ assert result is None
69
+
70
+
71
+ class TestStakingStatus:
72
+ """Tests for StakingStatus model."""
73
+
74
+ def test_staking_status_defaults(self):
75
+ """Test StakingStatus default values."""
76
+ status = StakingStatus(
77
+ is_staked=False,
78
+ staking_state="NOT_STAKED",
79
+ )
80
+
81
+ assert status.is_staked is False
82
+ assert status.staking_state == "NOT_STAKED"
83
+ assert status.mech_requests_this_epoch == 0
84
+ assert status.required_mech_requests == 0
85
+ assert status.remaining_mech_requests == 0
86
+ assert status.has_enough_requests is False
87
+ assert status.liveness_ratio_passed is False
88
+ assert status.accrued_reward_wei == 0
89
+
90
+ def test_staking_status_staked(self):
91
+ """Test StakingStatus when staked."""
92
+ status = StakingStatus(
93
+ is_staked=True,
94
+ staking_state="STAKED",
95
+ staking_contract_address="0x389B46c259631Acd6a69Bde8B6cEe218230bAE8C",
96
+ mech_requests_this_epoch=5,
97
+ required_mech_requests=3,
98
+ remaining_mech_requests=0,
99
+ has_enough_requests=True,
100
+ liveness_ratio_passed=True,
101
+ accrued_reward_wei=1000000000000000000,
102
+ )
103
+
104
+ assert status.is_staked is True
105
+ assert status.staking_contract_address == "0x389B46c259631Acd6a69Bde8B6cEe218230bAE8C"
106
+ assert status.has_enough_requests is True
107
+
108
+
109
+ class TestService:
110
+ """Tests for Service model."""
111
+
112
+ def test_service_key_property(self):
113
+ """Test Service key property generates correct key."""
114
+ service = Service(
115
+ service_name="test",
116
+ chain_name="gnosis",
117
+ service_id=123,
118
+ agent_ids=[25],
119
+ service_owner_address="0x1234567890123456789012345678901234567890",
120
+ )
121
+
122
+ assert service.key == "gnosis:123"
123
+
124
+ def test_service_with_optional_fields(self):
125
+ """Test Service with optional fields set."""
126
+ # Use valid Ethereum addresses (these are random but valid checksums)
127
+ # multisig_addr = "0x3f9Dd7c0e0D4D5f9f2F29F3f8A4c5D6e7F890123" # Corrected invalid chars
128
+ staking_addr = "0x389B46c259631Acd6a69Bde8B6cEe218230bAE8C"
129
+ token_addr = "0xcE11e14225575945b8E6Dc0D4F2dD4C570f79d9f"
130
+
131
+ service = Service(
132
+ service_name="test",
133
+ chain_name="gnosis",
134
+ service_id=456,
135
+ agent_ids=[25],
136
+ service_owner_address="0x1234567890123456789012345678901234567890",
137
+ staking_contract_address=staking_addr,
138
+ token_address=token_addr,
139
+ )
140
+
141
+ # These should be set correctly
142
+ assert service.staking_contract_address is not None
143
+ assert service.token_address is not None
144
+ assert service.key == "gnosis:456"
@@ -0,0 +1,258 @@
1
+ """Tests for Olas TUI View."""
2
+
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+ from textual.app import App, ComposeResult
7
+ from textual.widgets import Select
8
+
9
+ from iwa.plugins.olas.models import StakingStatus
10
+ from iwa.plugins.olas.tui.olas_view import OlasView
11
+ from iwa.tui.modals.base import CreateServiceModal, FundServiceModal
12
+
13
+ VALID_ADDR_1 = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
14
+ VALID_ADDR_2 = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
15
+ VALID_ADDR_3 = "0x1111111111111111111111111111111111111111"
16
+
17
+
18
+ class OlasTestApp(App):
19
+ """Test app to host OlasView."""
20
+
21
+ def __init__(self, wallet=None):
22
+ """Initialize test app."""
23
+ super().__init__()
24
+ self.wallet = wallet
25
+
26
+ def compose(self) -> ComposeResult:
27
+ """Compose layout."""
28
+ yield OlasView(self.wallet)
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_olas_view_initial_load(mock_wallet, mock_olas_config):
33
+ """Test OlasView initial loading and rendering."""
34
+ with patch("iwa.core.models.Config") as mock_config_cls:
35
+ mock_config = mock_config_cls.return_value
36
+ mock_config.plugins = {"olas": mock_olas_config.model_dump()}
37
+
38
+ with (
39
+ patch("iwa.plugins.olas.service_manager.ServiceManager") as mock_sm_cls,
40
+ patch("iwa.core.pricing.PriceService") as mock_price_cls,
41
+ ):
42
+ mock_sm = mock_sm_cls.return_value
43
+ # Default mock return value to avoid TypeErrors in background thread
44
+ mock_sm.get_staking_status.return_value = StakingStatus(
45
+ is_staked=False, staking_state="NOT_STAKED", remaining_epoch_seconds=3600
46
+ )
47
+
48
+ mock_sm.get_staking_status.return_value = StakingStatus(
49
+ is_staked=True,
50
+ staking_state="STAKED",
51
+ staking_contract_address="0xStaking",
52
+ staking_contract_name="Trader Staking",
53
+ accrued_reward_wei=500000000000000000,
54
+ liveness_ratio_passed=True,
55
+ remaining_epoch_seconds=3600,
56
+ epoch_number=1,
57
+ unstake_available_at="2025-12-24T12:00:00Z",
58
+ )
59
+ mock_sm.get_service_state.return_value = "DEPLOYED"
60
+ mock_price_cls.return_value.get_token_price.return_value = 1.23
61
+
62
+ app = OlasTestApp(mock_wallet)
63
+ async with app.run_test() as pilot:
64
+ view = app.query_one(OlasView)
65
+ assert view._chain == "gnosis"
66
+
67
+ # Wait for loading worker
68
+ await pilot.pause()
69
+
70
+ # Verify service card exists
71
+ assert bool(app.query("#card-gnosis_1"))
72
+ # Label content check
73
+ label = app.query_one(".service-title")
74
+ assert "Test Service #1" in label.render().plain
75
+
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_olas_view_chain_change(mock_wallet, mock_olas_config):
79
+ """Test changing chain in OlasView."""
80
+ with patch("iwa.core.models.Config") as mock_config_cls:
81
+ mock_config = mock_config_cls.return_value
82
+ mock_config.plugins = {"olas": mock_olas_config.model_dump()}
83
+
84
+ with (
85
+ patch("iwa.plugins.olas.service_manager.ServiceManager") as mock_sm_cls,
86
+ patch("iwa.plugins.olas.constants.OLAS_TRADER_STAKING_CONTRACTS", {"ethereum": []}),
87
+ patch("iwa.core.pricing.PriceService"),
88
+ ):
89
+ mock_sm = mock_sm_cls.return_value
90
+ mock_sm.get_services_full.return_value = []
91
+ mock_sm.get_staking_status.return_value = StakingStatus(
92
+ is_staked=False, staking_state="NOT_STAKED", remaining_epoch_seconds=3600
93
+ )
94
+ mock_sm.get_service_state.return_value = "DEPLOYED"
95
+ app = OlasTestApp(mock_wallet)
96
+ async with app.run_test() as pilot:
97
+ view = app.query_one(OlasView)
98
+
99
+ # Wait for initial load to finish
100
+ await pilot.pause(0.5)
101
+ for _ in range(50):
102
+ if not any(w.name == "load_services" for w in view.workers):
103
+ break
104
+ await pilot.pause(0.1)
105
+
106
+ # Change chain
107
+ select = app.query_one("#olas-chain-select", Select)
108
+ select.value = "ethereum"
109
+ await pilot.pause()
110
+ # Select.Changed will trigger load_services worker
111
+ await pilot.pause()
112
+
113
+ # Wait for worker to start and finish
114
+ await pilot.pause(0.5)
115
+ for _ in range(50):
116
+ if not any(w.name == "load_services" for w in view.workers):
117
+ break
118
+ await pilot.pause(0.1)
119
+
120
+ # Ensure call_from_thread tasks are also finished
121
+ await pilot.pause(0.5)
122
+
123
+ assert view._chain == "ethereum"
124
+ # Should show empty state since no eth services in mock
125
+ assert len(view.query(".empty-state")) > 0
126
+
127
+
128
+ @pytest.mark.asyncio
129
+ async def test_olas_view_actions(mock_wallet, mock_olas_config):
130
+ """Test button actions in OlasView."""
131
+ with patch("iwa.core.models.Config") as mock_config_cls:
132
+ mock_config = mock_config_cls.return_value
133
+ mock_config.plugins = {"olas": mock_olas_config.model_dump()}
134
+
135
+ with (
136
+ patch("iwa.plugins.olas.service_manager.ServiceManager") as mock_sm_cls,
137
+ patch("iwa.core.pricing.PriceService"),
138
+ ):
139
+ mock_sm = mock_sm_cls.return_value
140
+ mock_sm.get_staking_status.return_value = StakingStatus(
141
+ is_staked=True,
142
+ staking_state="STAKED",
143
+ accrued_reward_wei=10**18,
144
+ remaining_epoch_seconds=0, # Checkpoint pending
145
+ )
146
+ mock_sm.get_service_state.return_value = "DEPLOYED"
147
+
148
+ app = OlasTestApp(mock_wallet)
149
+ async with app.run_test() as pilot:
150
+ await pilot.pause()
151
+
152
+ # 1. Test Claim
153
+ with patch.object(OlasView, "claim_rewards") as mock_claim:
154
+ await pilot.click("#claim-gnosis_1")
155
+ mock_claim.assert_called_with("gnosis:1")
156
+
157
+ # 2. Test Unstake
158
+ with patch.object(OlasView, "unstake_service") as mock_unstake:
159
+ await pilot.click("#unstake-gnosis_1")
160
+ mock_unstake.assert_called_with("gnosis:1")
161
+
162
+ # 3. Test Checkpoint
163
+ with patch.object(OlasView, "checkpoint_service") as mock_checkpoint:
164
+ await pilot.click("#checkpoint-gnosis_1")
165
+ mock_checkpoint.assert_called_with("gnosis:1")
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_olas_view_create_service(mock_wallet, mock_olas_config):
170
+ """Test clicking Create Service button."""
171
+ with patch("iwa.core.models.Config") as mock_config_cls:
172
+ mock_config = mock_config_cls.return_value
173
+ mock_config.plugins = {"olas": mock_olas_config.model_dump()}
174
+
175
+ with (
176
+ patch("iwa.plugins.olas.service_manager.ServiceManager") as mock_sm_cls,
177
+ patch("iwa.core.pricing.PriceService"),
178
+ ):
179
+ mock_sm = mock_sm_cls.return_value
180
+ mock_sm.get_staking_status.return_value = StakingStatus(
181
+ is_staked=False, staking_state="NOT_STAKED"
182
+ )
183
+ mock_sm.get_service_state.return_value = "DEPLOYED"
184
+
185
+ app = OlasTestApp(mock_wallet)
186
+ async with app.run_test() as pilot:
187
+ # Wait for worker
188
+ view = app.query_one(OlasView)
189
+ for _ in range(10):
190
+ if not any(w.name == "load_services" for w in view.workers):
191
+ break
192
+ await pilot.pause(0.1)
193
+
194
+ # Patch push_screen on the app instance
195
+ with patch.object(app, "push_screen") as mock_push:
196
+ await pilot.click("#olas-create-service-btn")
197
+ await pilot.pause()
198
+
199
+ # Verify push_screen was called with a CreateServiceModal
200
+ assert mock_push.called
201
+ modal = mock_push.call_args[0][0]
202
+ assert isinstance(modal, CreateServiceModal)
203
+
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_olas_view_fund_service(mock_wallet, mock_olas_config):
207
+ """Test showing fund service modal."""
208
+ with patch("iwa.core.models.Config") as mock_config_cls:
209
+ mock_config = mock_config_cls.return_value
210
+ mock_config.plugins = {"olas": mock_olas_config.model_dump()}
211
+
212
+ with (
213
+ patch("iwa.plugins.olas.service_manager.ServiceManager") as mock_sm_cls,
214
+ patch("iwa.core.pricing.PriceService"),
215
+ ):
216
+ mock_sm = mock_sm_cls.return_value
217
+ mock_sm.get_staking_status.return_value = StakingStatus(
218
+ is_staked=False, staking_state="NOT_STAKED"
219
+ )
220
+ mock_sm.get_service_state.return_value = "DEPLOYED"
221
+
222
+ app = OlasTestApp(mock_wallet)
223
+ async with app.run_test() as pilot:
224
+ # Wait for worker
225
+ view = app.query_one(OlasView)
226
+ for _ in range(10):
227
+ if not any(w.name == "load_services" for w in view.workers):
228
+ break
229
+ await pilot.pause(0.1)
230
+
231
+ # Patch push_screen on the app instance
232
+ with patch.object(app, "push_screen") as mock_push:
233
+ await pilot.click("#fund-gnosis_1")
234
+ await pilot.pause()
235
+
236
+ # Verify push_screen was called with a FundServiceModal
237
+ assert mock_push.called
238
+ modal = mock_push.call_args[0][0]
239
+ assert isinstance(modal, FundServiceModal)
240
+
241
+
242
+ @pytest.mark.asyncio
243
+ async def test_olas_view_error_states(mock_wallet):
244
+ """Test OlasView error handling."""
245
+ # 1. No wallet
246
+ app = OlasTestApp(None)
247
+ async with app.run_test():
248
+ label = app.query_one(".empty-state")
249
+ assert "Wallet not available" in label.render().plain
250
+
251
+ # 2. No Olas configured
252
+ with patch("iwa.core.models.Config") as mock_config_cls:
253
+ mock_config = mock_config_cls.return_value
254
+ mock_config.plugins = {}
255
+ app = OlasTestApp(mock_wallet)
256
+ async with app.run_test():
257
+ label = app.query_one(".empty-state")
258
+ assert "No Olas services configured" in label.render().plain
@@ -0,0 +1,137 @@
1
+ """Tests for Olas TUI View actions."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+ from textual.app import App, ComposeResult
7
+
8
+ from iwa.plugins.olas.models import OlasConfig, Service, StakingStatus
9
+ from iwa.plugins.olas.tui.olas_view import OlasView
10
+
11
+ VALID_ADDR_1 = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
12
+ VALID_ADDR_2 = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D"
13
+ VALID_ADDR_3 = "0x1111111111111111111111111111111111111111"
14
+
15
+
16
+ @pytest.fixture
17
+ def mock_wallet():
18
+ """Mock wallet for testing."""
19
+ wallet = MagicMock()
20
+ wallet.balance_service = MagicMock()
21
+ wallet.key_storage = MagicMock()
22
+ wallet.get_native_balance_eth.return_value = 1.0
23
+ wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
24
+ wallet.key_storage.find_stored_account.return_value = None
25
+ wallet.master_account.address = VALID_ADDR_1
26
+ return wallet
27
+
28
+
29
+ @pytest.fixture
30
+ def mock_olas_config():
31
+ """Mock Olas configuration."""
32
+ service = Service(
33
+ service_id=1,
34
+ service_name="Test Service",
35
+ chain_name="gnosis",
36
+ agent_address=VALID_ADDR_1,
37
+ multisig_address=VALID_ADDR_2,
38
+ service_owner_address=VALID_ADDR_3,
39
+ staking_contract_address=VALID_ADDR_1,
40
+ )
41
+ config = OlasConfig(services={"gnosis:1": service})
42
+ return config
43
+
44
+
45
+ class OlasTestApp(App):
46
+ """Test app to host OlasView."""
47
+
48
+ def __init__(self, wallet=None):
49
+ """Initialize test app."""
50
+ super().__init__()
51
+ self.wallet = wallet
52
+
53
+ def compose(self) -> ComposeResult:
54
+ """Compose layout."""
55
+ yield OlasView(self.wallet)
56
+
57
+
58
+ async def wait_for_workers(view, pilot):
59
+ """Wait for all workers in OlasView to finish."""
60
+ for _ in range(50):
61
+ if not view.workers:
62
+ break
63
+ await pilot.pause(0.05)
64
+
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_olas_view_actions_suite(mock_wallet, mock_olas_config):
68
+ """Unified test for OlasView actions with robust mocking and synchronization."""
69
+ with patch("iwa.core.models.Config") as mock_conf_cls:
70
+ mock_conf = mock_conf_cls.return_value
71
+ mock_conf.plugins = {"olas": mock_olas_config.model_dump()}
72
+
73
+ # Patch both ServiceManager and StakingContract globally for the view
74
+ with (
75
+ patch("iwa.plugins.olas.service_manager.ServiceManager") as mock_sm_cls,
76
+ patch("iwa.plugins.olas.contracts.staking.StakingContract"),
77
+ ):
78
+ mock_sm = mock_sm_cls.return_value
79
+ # Default staking status to avoid TypeErrors during cards rendering
80
+ mock_sm.get_staking_status.return_value = StakingStatus(
81
+ is_staked=True,
82
+ staking_state="STAKED",
83
+ remaining_epoch_seconds=3600,
84
+ accrued_reward_wei=10**18,
85
+ )
86
+
87
+ app = OlasTestApp(mock_wallet)
88
+ async with app.run_test() as pilot:
89
+ view = app.query_one(OlasView)
90
+ await wait_for_workers(view, pilot)
91
+
92
+ # 1. Claim Rewards
93
+ mock_sm.claim_rewards.return_value = (True, 10**18)
94
+ view.claim_rewards("gnosis:1")
95
+ mock_sm.claim_rewards.assert_called_once()
96
+ await wait_for_workers(view, pilot) # success calls load_services
97
+
98
+ # 2. Unstake
99
+ mock_sm.unstake.return_value = True
100
+ view.unstake_service("gnosis:1")
101
+ mock_sm.unstake.assert_called_once()
102
+ await wait_for_workers(view, pilot)
103
+
104
+ # 3. Checkpoint
105
+ mock_sm.call_checkpoint.return_value = True
106
+ view.checkpoint_service("gnosis:1")
107
+ mock_sm.call_checkpoint.assert_called_once()
108
+ await wait_for_workers(view, pilot)
109
+
110
+ # 4. Drain
111
+ mock_sm.drain_service.return_value = {"safe": {"native": 1.0}}
112
+ view.drain_service("gnosis:1")
113
+ mock_sm.drain_service.assert_called_once()
114
+ await wait_for_workers(view, pilot)
115
+
116
+ # 5. Terminate (Wind Down)
117
+ mock_sm.wind_down.return_value = True
118
+ view.terminate_service("gnosis:1")
119
+ mock_sm.wind_down.assert_called_once()
120
+ await wait_for_workers(view, pilot)
121
+
122
+ # 6. Stake (via modal simulation)
123
+ from iwa.plugins.olas.constants import OLAS_TRADER_STAKING_CONTRACTS
124
+
125
+ with patch.dict(
126
+ OLAS_TRADER_STAKING_CONTRACTS, {"gnosis": {"Contract": VALID_ADDR_1}}
127
+ ):
128
+ with patch.object(app, "push_screen") as mock_push:
129
+ view.stake_service("gnosis:1")
130
+ assert mock_push.called
131
+
132
+ # Get callback from push_screen
133
+ callback = mock_push.call_args[0][1] # modal, callback
134
+ mock_sm.stake.return_value = True
135
+ callback(VALID_ADDR_1)
136
+ mock_sm.stake.assert_called_once()
137
+ await wait_for_workers(view, pilot)
@@ -0,0 +1,120 @@
1
+ """Modal callback tests for OlasView."""
2
+
3
+ from unittest.mock import MagicMock, PropertyMock, patch
4
+
5
+ import pytest
6
+
7
+ from iwa.plugins.olas.tui.olas_view import OlasView
8
+
9
+
10
+ @pytest.mark.asyncio
11
+ async def test_olas_view_modal_callbacks_full(mock_wallet):
12
+ """Test OlasView modal callbacks directly for coverage."""
13
+ # Patch ServiceManager globally for the view init
14
+ with patch("iwa.plugins.olas.service_manager.ServiceManager"):
15
+ view = OlasView(wallet=mock_wallet)
16
+ view.load_services = MagicMock()
17
+ view.notify = MagicMock()
18
+ mock_app = MagicMock()
19
+
20
+ # Patch the read-only property 'app'
21
+ with patch.object(OlasView, "app", new_callable=PropertyMock) as mock_app_prop:
22
+ mock_app_prop.return_value = mock_app
23
+
24
+ # 1. Create Service callback (Full flow)
25
+ view.show_create_service_modal()
26
+ assert mock_app.push_screen.called
27
+ args, kwargs = mock_app.push_screen.call_args
28
+ callback = kwargs.get("callback") or (args[1] if len(args) > 1 else None)
29
+
30
+ if callback:
31
+ with patch("iwa.plugins.olas.service_manager.ServiceManager") as mock_sm_cls:
32
+ mock_sm = mock_sm_cls.return_value
33
+ mock_sm.create.return_value = 123
34
+
35
+ # Sub-case: created but deploy failed
36
+ mock_sm.spin_up.return_value = False
37
+ callback({"chain": "gnosis", "name": "test", "staking_contract": None})
38
+ view.notify.assert_any_call(
39
+ "Service created (ID: 123) but deployment failed", severity="warning"
40
+ )
41
+
42
+ # Sub-case: success with staking
43
+ mock_sm.spin_up.return_value = True
44
+ callback({"chain": "gnosis", "name": "test", "staking_contract": "0x1"})
45
+ view.notify.assert_any_call(
46
+ "Service deployed and staked! ID: 123", severity="information"
47
+ )
48
+
49
+ # Sub-case: exception
50
+ mock_sm.create.side_effect = Exception("creation error")
51
+ callback({"chain": "gnosis", "name": "test"})
52
+ view.notify.assert_any_call("Error: creation error", severity="error")
53
+
54
+ view.load_services.reset_mock()
55
+ mock_app.push_screen.reset_mock()
56
+
57
+ # 2. Fund Service callback (Full flow)
58
+ view.show_fund_service_modal("gnosis:1")
59
+ assert mock_app.push_screen.called
60
+ args, kwargs = mock_app.push_screen.call_args
61
+ callback = kwargs.get("callback") or (args[1] if len(args) > 1 else None)
62
+
63
+ if callback:
64
+ with patch("iwa.core.models.Config") as mock_conf_cls:
65
+ mock_conf_instance = mock_conf_cls.return_value
66
+ addr = "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
67
+ mock_conf_instance.plugins = {
68
+ "olas": {
69
+ "services": {
70
+ "gnosis:1": {
71
+ "agent_address": addr,
72
+ "multisig_address": addr,
73
+ "chain_name": "gnosis",
74
+ "service_name": "test",
75
+ "service_id": 1,
76
+ }
77
+ }
78
+ }
79
+ }
80
+ callback({"agent_amount": 1.0, "safe_amount": 0.5})
81
+ assert mock_wallet.send.call_count == 2
82
+ view.notify.assert_any_call("Service funded!", severity="information")
83
+ view.load_services.assert_called_once()
84
+
85
+ # Sub-case: skip amount 0
86
+ mock_wallet.send.reset_mock()
87
+ callback({"agent_amount": 0.0, "safe_amount": 0.0})
88
+ assert mock_wallet.send.call_count == 0
89
+
90
+ view.load_services.reset_mock()
91
+ mock_app.push_screen.reset_mock()
92
+
93
+ # 3. Stake Service callback
94
+ view._chain = "gnosis"
95
+ with patch(
96
+ "iwa.plugins.olas.constants.OLAS_TRADER_STAKING_CONTRACTS",
97
+ {"gnosis": {"test": "0x1"}},
98
+ ):
99
+ view.stake_service("gnosis:1")
100
+ assert mock_app.push_screen.called
101
+ args, kwargs = mock_app.push_screen.call_args
102
+ callback = kwargs.get("callback") or (args[1] if len(args) > 1 else None)
103
+ if callback:
104
+ with (
105
+ patch(
106
+ "iwa.plugins.olas.service_manager.ServiceManager"
107
+ ) as mock_sm_inner_cls,
108
+ patch("iwa.core.contracts.contract.ChainInterfaces") as mock_ci,
109
+ ):
110
+ mock_ci.get_instance.return_value.web3.eth.contract.return_value = (
111
+ MagicMock()
112
+ )
113
+ mock_sm_inner = mock_sm_inner_cls.return_value
114
+ mock_sm_inner.stake.return_value = True
115
+ try:
116
+ # Use a valid checksum address
117
+ callback("0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB")
118
+ except Exception as e:
119
+ pytest.fail(f"Stake callback failed: {e}")
120
+ view.load_services.assert_called_once()