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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {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()
|