iwa 0.0.2__py3-none-any.whl → 0.0.11__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.
- iwa/core/chain/interface.py +51 -30
- iwa/core/chain/models.py +9 -15
- iwa/core/contracts/contract.py +8 -2
- iwa/core/pricing.py +10 -8
- iwa/core/services/safe.py +13 -8
- iwa/core/services/transaction.py +211 -7
- iwa/core/utils.py +22 -0
- iwa/core/wallet.py +2 -1
- iwa/plugins/gnosis/safe.py +4 -3
- iwa/plugins/gnosis/tests/test_safe.py +9 -7
- iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +926 -0
- iwa/plugins/olas/contracts/service.py +54 -4
- iwa/plugins/olas/contracts/staking.py +2 -3
- iwa/plugins/olas/plugin.py +14 -7
- iwa/plugins/olas/service_manager/lifecycle.py +382 -85
- iwa/plugins/olas/service_manager/mech.py +1 -1
- iwa/plugins/olas/service_manager/staking.py +229 -82
- iwa/plugins/olas/tests/test_olas_contracts.py +6 -2
- iwa/plugins/olas/tests/test_plugin.py +6 -1
- iwa/plugins/olas/tests/test_plugin_full.py +12 -7
- iwa/plugins/olas/tests/test_service_lifecycle.py +1 -4
- iwa/plugins/olas/tests/test_service_manager.py +59 -89
- iwa/plugins/olas/tests/test_service_manager_errors.py +1 -2
- iwa/plugins/olas/tests/test_service_manager_flows.py +5 -15
- iwa/plugins/olas/tests/test_service_manager_validation.py +16 -15
- iwa/tools/list_contracts.py +2 -2
- iwa/web/dependencies.py +1 -3
- iwa/web/routers/accounts.py +1 -2
- iwa/web/routers/olas/admin.py +1 -3
- iwa/web/routers/olas/funding.py +1 -3
- iwa/web/routers/olas/general.py +1 -3
- iwa/web/routers/olas/services.py +53 -21
- iwa/web/routers/olas/staking.py +27 -24
- iwa/web/routers/swap.py +1 -2
- iwa/web/routers/transactions.py +0 -2
- iwa/web/server.py +8 -6
- iwa/web/static/app.js +22 -0
- iwa/web/tests/test_web_endpoints.py +1 -1
- iwa/web/tests/test_web_olas.py +1 -1
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/METADATA +1 -1
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/RECORD +58 -56
- tests/test_chain.py +12 -7
- tests/test_chain_interface_coverage.py +3 -2
- tests/test_contract.py +165 -0
- tests/test_keys.py +2 -1
- tests/test_legacy_wallet.py +11 -0
- tests/test_pricing.py +32 -15
- tests/test_safe_coverage.py +3 -3
- tests/test_safe_service.py +3 -6
- tests/test_service_transaction.py +8 -3
- tests/test_staking_router.py +6 -3
- tests/test_transaction_service.py +4 -0
- tools/create_and_stake_service.py +103 -0
- tools/verify_drain.py +1 -4
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/WHEEL +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/top_level.txt +0 -0
tests/test_contract.py
CHANGED
|
@@ -195,3 +195,168 @@ def test_extract_events_edge_cases(mock_chain_interface):
|
|
|
195
195
|
|
|
196
196
|
events = contract.extract_events(receipt)
|
|
197
197
|
assert len(events) == 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# =============================================================================
|
|
201
|
+
# RPC ROTATION TESTS - Verify fix for contract.call() re-evaluating on retry
|
|
202
|
+
# =============================================================================
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_call_reevaluates_contract_on_retry(mock_chain_interface, mock_abi_file):
|
|
206
|
+
"""Verify that self.contract is re-evaluated on each retry attempt.
|
|
207
|
+
|
|
208
|
+
This test verifies the fix for the bug where the contract method was
|
|
209
|
+
captured once outside the retry lambda, causing retries to use the
|
|
210
|
+
stale provider after RPC rotation.
|
|
211
|
+
"""
|
|
212
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
213
|
+
|
|
214
|
+
# Track how many times web3.eth.contract is called (proxy for contract property access)
|
|
215
|
+
contract_creation_count = [0]
|
|
216
|
+
|
|
217
|
+
def counting_contract_factory(address, abi):
|
|
218
|
+
contract_creation_count[0] += 1
|
|
219
|
+
mock = MagicMock()
|
|
220
|
+
# First call fails, second succeeds
|
|
221
|
+
if contract_creation_count[0] == 1:
|
|
222
|
+
mock.functions.testFunc.return_value.call.side_effect = Exception(
|
|
223
|
+
"429 Too Many Requests"
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
mock.functions.testFunc.return_value.call.return_value = "success"
|
|
227
|
+
return mock
|
|
228
|
+
|
|
229
|
+
mock_chain_interface.web3.eth.contract.side_effect = counting_contract_factory
|
|
230
|
+
|
|
231
|
+
# Implement with_retry that actually retries on 429
|
|
232
|
+
def real_with_retry(fn, max_retries=6, operation_name="operation"):
|
|
233
|
+
for attempt in range(max_retries + 1):
|
|
234
|
+
try:
|
|
235
|
+
return fn()
|
|
236
|
+
except Exception as e:
|
|
237
|
+
if "429" in str(e) and attempt < max_retries:
|
|
238
|
+
continue
|
|
239
|
+
raise
|
|
240
|
+
|
|
241
|
+
mock_chain_interface.with_retry.side_effect = real_with_retry
|
|
242
|
+
|
|
243
|
+
# Execute - should fail first, then succeed
|
|
244
|
+
result = contract.call("testFunc")
|
|
245
|
+
|
|
246
|
+
assert result == "success"
|
|
247
|
+
# KEY ASSERTION: contract property (and thus web3.eth.contract) should be called
|
|
248
|
+
# once per attempt. With the fix, this should be 2. Before the fix, it would be 1.
|
|
249
|
+
assert contract_creation_count[0] == 2, (
|
|
250
|
+
f"Expected contract to be created 2 times (once per retry attempt), "
|
|
251
|
+
f"but was created {contract_creation_count[0]} times. "
|
|
252
|
+
"This suggests the fix for re-evaluating self.contract on retry is not working."
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_call_uses_fresh_provider_after_rotation(mock_chain_interface, mock_abi_file):
|
|
257
|
+
"""Verify that after RPC rotation, the contract uses the new provider.
|
|
258
|
+
|
|
259
|
+
This simulates the scenario where:
|
|
260
|
+
1. First call fails with 429
|
|
261
|
+
2. RPC rotates to new provider
|
|
262
|
+
3. Retry should use the NEW provider, not the old one
|
|
263
|
+
"""
|
|
264
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
265
|
+
|
|
266
|
+
# Track which provider version is being used
|
|
267
|
+
provider_versions = []
|
|
268
|
+
current_provider_version = [1] # Mutable to track version changes
|
|
269
|
+
|
|
270
|
+
def mock_contract_factory(address, abi):
|
|
271
|
+
mock = MagicMock()
|
|
272
|
+
# Capture which provider version was used when this contract was created
|
|
273
|
+
mock._provider_version = current_provider_version[0]
|
|
274
|
+
provider_versions.append(current_provider_version[0])
|
|
275
|
+
return mock
|
|
276
|
+
|
|
277
|
+
mock_chain_interface.web3.eth.contract.side_effect = mock_contract_factory
|
|
278
|
+
|
|
279
|
+
# Simulate RPC rotation by incrementing provider version
|
|
280
|
+
def simulate_rotation():
|
|
281
|
+
current_provider_version[0] += 1
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
mock_chain_interface.rotate_rpc = simulate_rotation
|
|
285
|
+
|
|
286
|
+
# Make with_retry call rotation between attempts
|
|
287
|
+
attempt_count = [0]
|
|
288
|
+
|
|
289
|
+
def mock_with_retry(fn, **kwargs):
|
|
290
|
+
attempt_count[0] += 1
|
|
291
|
+
if attempt_count[0] == 1:
|
|
292
|
+
# First attempt: simulate 429 then rotation
|
|
293
|
+
simulate_rotation()
|
|
294
|
+
# Call again after rotation
|
|
295
|
+
return fn()
|
|
296
|
+
return fn()
|
|
297
|
+
|
|
298
|
+
mock_chain_interface.with_retry.side_effect = mock_with_retry
|
|
299
|
+
|
|
300
|
+
# Execute call (this should access contract property, which creates new contract)
|
|
301
|
+
contract.call("testFunc")
|
|
302
|
+
|
|
303
|
+
# Verify: contract was created at least once with the rotated provider
|
|
304
|
+
# If the fix works, the provider_versions list should show provider version 2
|
|
305
|
+
# (because rotation happened before the successful call)
|
|
306
|
+
assert len(provider_versions) >= 1
|
|
307
|
+
# The last contract should have been created with the rotated provider
|
|
308
|
+
assert provider_versions[-1] == 2, f"Expected provider version 2, got {provider_versions}"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_call_with_429_triggers_retry_with_new_contract(mock_chain_interface, mock_abi_file):
|
|
312
|
+
"""Integration test: 429 error should trigger retry which uses fresh contract.
|
|
313
|
+
|
|
314
|
+
This test verifies the complete flow:
|
|
315
|
+
1. Call fails with 429
|
|
316
|
+
2. with_retry handles it and retries
|
|
317
|
+
3. The retry uses a fresh contract instance (new provider)
|
|
318
|
+
"""
|
|
319
|
+
contract = MockContract("0xAddress", "gnosis")
|
|
320
|
+
|
|
321
|
+
# Create distinct mock contracts for each call
|
|
322
|
+
mock_contract_1 = MagicMock()
|
|
323
|
+
mock_contract_1.functions.testFunc.return_value.call.side_effect = Exception(
|
|
324
|
+
"429 Too Many Requests"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
mock_contract_2 = MagicMock()
|
|
328
|
+
mock_contract_2.functions.testFunc.return_value.call.return_value = "success_from_rotated_rpc"
|
|
329
|
+
|
|
330
|
+
contracts_returned = [mock_contract_1, mock_contract_2]
|
|
331
|
+
contract_call_count = [0]
|
|
332
|
+
|
|
333
|
+
def mock_contract_factory(address, abi):
|
|
334
|
+
result = contracts_returned[min(contract_call_count[0], len(contracts_returned) - 1)]
|
|
335
|
+
contract_call_count[0] += 1
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
mock_chain_interface.web3.eth.contract.side_effect = mock_contract_factory
|
|
339
|
+
|
|
340
|
+
# Implement with_retry that actually retries
|
|
341
|
+
def real_with_retry(fn, max_retries=6, operation_name="operation"):
|
|
342
|
+
last_error = None
|
|
343
|
+
for attempt in range(max_retries + 1):
|
|
344
|
+
try:
|
|
345
|
+
return fn()
|
|
346
|
+
except Exception as e:
|
|
347
|
+
last_error = e
|
|
348
|
+
if "429" in str(e) and attempt < max_retries:
|
|
349
|
+
continue # Retry
|
|
350
|
+
raise
|
|
351
|
+
raise last_error
|
|
352
|
+
|
|
353
|
+
mock_chain_interface.with_retry.side_effect = real_with_retry
|
|
354
|
+
|
|
355
|
+
# Execute - should succeed on second attempt with rotated RPC
|
|
356
|
+
result = contract.call("testFunc")
|
|
357
|
+
|
|
358
|
+
assert result == "success_from_rotated_rpc"
|
|
359
|
+
# Verify we created 2 contract instances (one per attempt)
|
|
360
|
+
assert contract_call_count[0] == 2, (
|
|
361
|
+
f"Expected 2 contract creations, got {contract_call_count[0]}"
|
|
362
|
+
)
|
tests/test_keys.py
CHANGED
|
@@ -67,7 +67,8 @@ def mock_account():
|
|
|
67
67
|
def from_key_side_effect(private_key):
|
|
68
68
|
# 1. Handle the master private key
|
|
69
69
|
if (
|
|
70
|
-
private_key
|
|
70
|
+
private_key
|
|
71
|
+
== "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # gitleaks:allow
|
|
71
72
|
):
|
|
72
73
|
addr = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
|
|
73
74
|
# 2. Handle our mock key format
|
tests/test_legacy_wallet.py
CHANGED
|
@@ -39,6 +39,17 @@ def mock_transaction_service():
|
|
|
39
39
|
yield instance
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
@pytest.fixture(autouse=True)
|
|
43
|
+
def mock_chain_sleeps():
|
|
44
|
+
"""Mock time.sleep in chain modules to speed up tests."""
|
|
45
|
+
with (
|
|
46
|
+
patch("iwa.core.chain.interface.time.sleep"),
|
|
47
|
+
patch("iwa.core.chain.rate_limiter.time.sleep"),
|
|
48
|
+
patch("time.sleep"),
|
|
49
|
+
):
|
|
50
|
+
yield
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
@pytest.fixture
|
|
43
54
|
def mock_key_storage():
|
|
44
55
|
with patch("iwa.core.wallet.KeyStorage") as mock:
|
tests/test_pricing.py
CHANGED
|
@@ -18,6 +18,13 @@ def price_service(mock_secrets):
|
|
|
18
18
|
return PriceService()
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
@pytest.fixture(autouse=True)
|
|
22
|
+
def clear_cache():
|
|
23
|
+
"""Clear global cache before each test to ensure isolation."""
|
|
24
|
+
with patch("iwa.core.pricing._PRICE_CACHE", {}):
|
|
25
|
+
yield
|
|
26
|
+
|
|
27
|
+
|
|
21
28
|
def test_get_token_price_success(price_service):
|
|
22
29
|
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
23
30
|
mock_get.return_value.status_code = 200
|
|
@@ -36,30 +43,36 @@ def test_get_token_price_cached(price_service):
|
|
|
36
43
|
# Pre-populate cache
|
|
37
44
|
from datetime import datetime
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
cache_data = {
|
|
47
|
+
"ethereum_eur": {"price": 100.0, "timestamp": datetime.now()}
|
|
48
|
+
}
|
|
40
49
|
|
|
41
|
-
with patch("iwa.core.pricing.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
with patch.dict("iwa.core.pricing._PRICE_CACHE", cache_data):
|
|
51
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
52
|
+
price = price_service.get_token_price("ethereum", "eur")
|
|
53
|
+
assert price == 100.0
|
|
54
|
+
mock_get.assert_not_called()
|
|
45
55
|
|
|
46
56
|
|
|
47
57
|
def test_get_token_price_cache_expired(price_service):
|
|
48
58
|
# Pre-populate expired cache
|
|
49
59
|
from datetime import datetime
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
61
|
+
cache_data = {
|
|
62
|
+
"ethereum_eur": {
|
|
63
|
+
"price": 100.0,
|
|
64
|
+
"timestamp": datetime.now() - timedelta(minutes=60), # > 30 min TTL
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
|
|
56
|
-
with patch("iwa.core.pricing.
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
with patch.dict("iwa.core.pricing._PRICE_CACHE", cache_data):
|
|
69
|
+
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
70
|
+
mock_get.return_value.status_code = 200
|
|
71
|
+
mock_get.return_value.json.return_value = {"ethereum": {"eur": 200.0}}
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
price = price_service.get_token_price("ethereum", "eur")
|
|
74
|
+
assert price == 200.0
|
|
75
|
+
mock_get.assert_called_once()
|
|
63
76
|
|
|
64
77
|
|
|
65
78
|
def test_get_token_price_api_error(price_service):
|
|
@@ -84,7 +97,9 @@ def test_get_token_price_rate_limit():
|
|
|
84
97
|
with patch("iwa.core.pricing.secrets") as mock_secrets:
|
|
85
98
|
mock_secrets.coingecko_api_key = None
|
|
86
99
|
|
|
100
|
+
# Need to re-instantiate or patch secrets on instance since it's read in __init__
|
|
87
101
|
service = PriceService()
|
|
102
|
+
service.api_key = None
|
|
88
103
|
|
|
89
104
|
with patch("iwa.core.pricing.requests.get") as mock_get, patch("time.sleep"):
|
|
90
105
|
# Return 429 for all attempts
|
|
@@ -94,7 +109,7 @@ def test_get_token_price_rate_limit():
|
|
|
94
109
|
price = service.get_token_price("ethereum", "eur")
|
|
95
110
|
|
|
96
111
|
assert price is None
|
|
97
|
-
# Should have tried max_retries + 1 times
|
|
112
|
+
# Should have tried max_retries + 1 times (3 total)
|
|
98
113
|
assert mock_get.call_count == 3
|
|
99
114
|
|
|
100
115
|
|
|
@@ -106,6 +121,7 @@ def test_get_token_price_rate_limit_then_success():
|
|
|
106
121
|
mock_secrets.coingecko_api_key = None
|
|
107
122
|
|
|
108
123
|
service = PriceService()
|
|
124
|
+
service.api_key = None
|
|
109
125
|
|
|
110
126
|
with patch("iwa.core.pricing.requests.get") as mock_get, patch("time.sleep"):
|
|
111
127
|
# First call returns 429, second succeeds
|
|
@@ -130,6 +146,7 @@ def test_get_token_price_no_api_key():
|
|
|
130
146
|
mock_secrets.coingecko_api_key = None
|
|
131
147
|
|
|
132
148
|
service = PriceService()
|
|
149
|
+
service.api_key = None
|
|
133
150
|
|
|
134
151
|
with patch("iwa.core.pricing.requests.get") as mock_get:
|
|
135
152
|
mock_get.return_value.status_code = 200
|
tests/test_safe_coverage.py
CHANGED
|
@@ -56,7 +56,7 @@ def test_execute_safe_transaction_success(safe_service, mock_deps):
|
|
|
56
56
|
mock_safe_instance = mock_safe_multisig_cls.return_value
|
|
57
57
|
mock_safe_tx = MagicMock()
|
|
58
58
|
mock_safe_instance.build_tx.return_value = mock_safe_tx
|
|
59
|
-
mock_safe_tx.tx_hash.hex.return_value = "
|
|
59
|
+
mock_safe_tx.tx_hash.hex.return_value = "TxHash"
|
|
60
60
|
|
|
61
61
|
# Execute
|
|
62
62
|
tx_hash = safe_service.execute_safe_transaction(safe_address, to_address, value, chain_name)
|
|
@@ -90,7 +90,7 @@ def test_get_sign_and_execute_callback(safe_service, mock_deps):
|
|
|
90
90
|
|
|
91
91
|
# Test executing callback
|
|
92
92
|
mock_safe_tx = MagicMock()
|
|
93
|
-
mock_safe_tx.tx_hash.hex.return_value = "
|
|
93
|
+
mock_safe_tx.tx_hash.hex.return_value = "TxHash"
|
|
94
94
|
|
|
95
95
|
result = callback(mock_safe_tx)
|
|
96
96
|
|
|
@@ -119,7 +119,7 @@ def test_redeploy_safes(safe_service, mock_deps):
|
|
|
119
119
|
|
|
120
120
|
mock_deps["key_storage"].accounts = {"0xSafe1": account1}
|
|
121
121
|
|
|
122
|
-
with patch("iwa.core.
|
|
122
|
+
with patch("iwa.core.chain.models.secrets") as mock_settings:
|
|
123
123
|
mock_settings.gnosis_rpc.get_secret_value.return_value = "http://rpc"
|
|
124
124
|
|
|
125
125
|
with patch("iwa.core.services.safe.EthereumClient") as mock_eth_client:
|
tests/test_safe_service.py
CHANGED
|
@@ -53,18 +53,16 @@ def mock_dependencies():
|
|
|
53
53
|
patch("iwa.core.services.safe.EthereumClient") as mock_client,
|
|
54
54
|
patch("iwa.core.services.safe.Safe") as mock_safe,
|
|
55
55
|
patch("iwa.core.services.safe.ProxyFactory") as mock_proxy_factory,
|
|
56
|
-
patch("iwa.core.services.safe.secrets") as mock_secrets,
|
|
57
56
|
patch("iwa.core.services.safe.log_transaction") as mock_log,
|
|
58
57
|
patch("iwa.core.services.safe.get_safe_master_copy_address") as mock_master,
|
|
59
58
|
patch("iwa.core.services.safe.get_safe_proxy_factory_address") as mock_factory,
|
|
59
|
+
patch("time.sleep"), # Avoid any retry delays
|
|
60
60
|
):
|
|
61
|
-
mock_secrets.gnosis_rpc.get_secret_value.return_value = "http://rpc"
|
|
62
|
-
|
|
63
61
|
# Setup Safe creation return
|
|
64
62
|
mock_create_tx = MagicMock()
|
|
65
63
|
# Valid Checksum Address - New Safe (Matches Pydantic output)
|
|
66
64
|
mock_create_tx.contract_address = "0xbEC49fa140ACaa83533f900357DCD37866d50618"
|
|
67
|
-
mock_create_tx.tx_hash.hex.return_value = "
|
|
65
|
+
mock_create_tx.tx_hash.hex.return_value = "TxHash"
|
|
68
66
|
|
|
69
67
|
mock_safe.create.return_value = mock_create_tx
|
|
70
68
|
|
|
@@ -72,7 +70,7 @@ def mock_dependencies():
|
|
|
72
70
|
mock_deploy_tx = MagicMock()
|
|
73
71
|
# Valid checksum address - Salted Safe
|
|
74
72
|
mock_deploy_tx.contract_address = "0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"
|
|
75
|
-
mock_deploy_tx.tx_hash.hex.return_value = "
|
|
73
|
+
mock_deploy_tx.tx_hash.hex.return_value = "TxHashSalted"
|
|
76
74
|
|
|
77
75
|
mock_proxy_factory.return_value.deploy_proxy_contract_with_nonce.return_value = (
|
|
78
76
|
mock_deploy_tx
|
|
@@ -104,7 +102,6 @@ def mock_dependencies():
|
|
|
104
102
|
"client": mock_client,
|
|
105
103
|
"safe": mock_safe,
|
|
106
104
|
"proxy_factory": mock_proxy_factory,
|
|
107
|
-
"secrets": mock_secrets,
|
|
108
105
|
"log": mock_log,
|
|
109
106
|
"master": mock_master,
|
|
110
107
|
"factory": mock_factory,
|
|
@@ -17,6 +17,10 @@ def mock_chain_interfaces():
|
|
|
17
17
|
gnosis_interface.chain = mock_chain
|
|
18
18
|
gnosis_interface.web3 = MagicMock()
|
|
19
19
|
instance.get.return_value = gnosis_interface
|
|
20
|
+
|
|
21
|
+
# Mock with_retry to execute the operation
|
|
22
|
+
gnosis_interface.with_retry.side_effect = lambda op, **kwargs: op()
|
|
23
|
+
|
|
20
24
|
yield instance
|
|
21
25
|
|
|
22
26
|
|
|
@@ -115,8 +119,8 @@ def test_sign_and_send_max_retries_exhausted(
|
|
|
115
119
|
# Should fail after max retries
|
|
116
120
|
assert success is False
|
|
117
121
|
assert receipt == {} # Returns empty dict on failure
|
|
118
|
-
# Should have tried
|
|
119
|
-
assert chain_interface.web3.eth.send_raw_transaction.call_count ==
|
|
122
|
+
# Should have tried 10 times (max_retries)
|
|
123
|
+
assert chain_interface.web3.eth.send_raw_transaction.call_count == 10
|
|
120
124
|
|
|
121
125
|
|
|
122
126
|
def test_sign_and_send_transaction_reverted(
|
|
@@ -170,7 +174,8 @@ def test_sign_and_send_signer_not_found(
|
|
|
170
174
|
# Signing raises ValueError for unknown account
|
|
171
175
|
mock_key_storage.sign_transaction.side_effect = ValueError("Account not found")
|
|
172
176
|
|
|
173
|
-
|
|
177
|
+
with patch("time.sleep"): # Avoid real retry delays
|
|
178
|
+
success, receipt = transaction_service.sign_and_send(tx, "unknown_signer")
|
|
174
179
|
|
|
175
180
|
assert success is False
|
|
176
181
|
assert receipt == {} # Returns empty dict on failure
|
tests/test_staking_router.py
CHANGED
|
@@ -19,10 +19,13 @@ def test_check_availability_exception():
|
|
|
19
19
|
"""Test _check_availability handles contract call failures."""
|
|
20
20
|
from iwa.web.routers.olas.staking import _check_availability
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
mock_interface = MagicMock()
|
|
23
|
+
mock_interface.chain.name = "gnosis"
|
|
24
|
+
|
|
25
|
+
with patch("iwa.plugins.olas.contracts.staking.StakingContract") as mock_contract_cls:
|
|
26
|
+
mock_contract_cls.side_effect = Exception("Contract error")
|
|
27
|
+
result = _check_availability("Test", "0xAddr", mock_interface)
|
|
24
28
|
|
|
25
|
-
result = _check_availability("Test", "0xAddr", mock_w3, [])
|
|
26
29
|
assert result["name"] == "Test"
|
|
27
30
|
assert result["usage"] is None
|
|
28
31
|
|
|
@@ -55,6 +55,10 @@ def mock_chain_interfaces():
|
|
|
55
55
|
gnosis_interface.web3.eth.wait_for_transaction_receipt.return_value = mock_receipt
|
|
56
56
|
|
|
57
57
|
instance.get.return_value = gnosis_interface
|
|
58
|
+
|
|
59
|
+
# Mock with_retry to execute the operation
|
|
60
|
+
gnosis_interface.with_retry.side_effect = lambda op, **kwargs: op()
|
|
61
|
+
|
|
58
62
|
yield instance
|
|
59
63
|
|
|
60
64
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create a service with correct bond and stake it in Expert 7 MM contract.
|
|
3
|
+
|
|
4
|
+
This script demonstrates the minimum steps to:
|
|
5
|
+
1. Get the required bond amount from the staking contract
|
|
6
|
+
2. Create a service with that bond (NOT 1 wei)
|
|
7
|
+
3. Spin up the service (activate → register → deploy)
|
|
8
|
+
4. Stake the service in the staking contract
|
|
9
|
+
|
|
10
|
+
IMPORTANT: The service MUST be created with the correct bond amount
|
|
11
|
+
specified by the staking contract. Creating with bond=1 wei will
|
|
12
|
+
cause staking to fail because the on-chain bond won't match requirements.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
# Add src to path
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
20
|
+
|
|
21
|
+
from iwa.core.wallet import Wallet
|
|
22
|
+
from iwa.plugins.olas.constants import OLAS_TRADER_STAKING_CONTRACTS
|
|
23
|
+
from iwa.plugins.olas.contracts.staking import StakingContract
|
|
24
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
"""Create and stake a service in Expert 7 MM contract."""
|
|
29
|
+
print("=" * 60)
|
|
30
|
+
print(" Create & Stake Service - Expert 7 MM (10k OLAS)")
|
|
31
|
+
print("=" * 60)
|
|
32
|
+
|
|
33
|
+
# 1. Initialize wallet
|
|
34
|
+
print("\n[1] Initializing Wallet...")
|
|
35
|
+
wallet = Wallet()
|
|
36
|
+
master_address = wallet.master_account.address if wallet.master_account else "N/A"
|
|
37
|
+
print(f" OK: Master account: {master_address}")
|
|
38
|
+
|
|
39
|
+
# 2. Get the staking contract
|
|
40
|
+
print("\n[2] Loading staking contract...")
|
|
41
|
+
staking_address = OLAS_TRADER_STAKING_CONTRACTS["gnosis"]["Expert 7 MM (10k OLAS)"]
|
|
42
|
+
staking_contract = StakingContract(staking_address, chain_name="gnosis")
|
|
43
|
+
print(f" OK: Staking contract: {staking_address}")
|
|
44
|
+
|
|
45
|
+
# 3. Get the required bond amount from the staking contract
|
|
46
|
+
print("\n[3] Getting staking requirements...")
|
|
47
|
+
requirements = staking_contract.get_requirements()
|
|
48
|
+
required_bond = requirements["required_agent_bond"]
|
|
49
|
+
min_deposit = requirements["min_staking_deposit"]
|
|
50
|
+
staking_token = str(requirements["staking_token"])
|
|
51
|
+
print(f" - Required agent bond: {required_bond} wei")
|
|
52
|
+
print(f" - Min staking deposit: {min_deposit} wei")
|
|
53
|
+
print(f" - Staking token: {staking_token}")
|
|
54
|
+
|
|
55
|
+
# 4. Create service with the correct bond amount
|
|
56
|
+
print("\n[4] Creating Service with correct bond...")
|
|
57
|
+
manager = ServiceManager(wallet)
|
|
58
|
+
service_id = manager.create(
|
|
59
|
+
chain_name="gnosis",
|
|
60
|
+
service_name="staked_service_7mm",
|
|
61
|
+
token_address_or_tag=staking_token, # Use OLAS token
|
|
62
|
+
bond_amount_wei=required_bond, # THIS IS THE KEY: use required bond, not 1 wei
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not service_id:
|
|
66
|
+
print(" FAIL: Failed to create service")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
print(f" OK: Service created with ID: {service_id}")
|
|
70
|
+
|
|
71
|
+
# 5. Spin up the service (activate -> register -> deploy -> stake)
|
|
72
|
+
print("\n[5] Spinning up and staking Service...")
|
|
73
|
+
success = manager.spin_up(
|
|
74
|
+
bond_amount_wei=required_bond,
|
|
75
|
+
staking_contract=staking_contract, # spin_up handles staking automatically
|
|
76
|
+
)
|
|
77
|
+
if not success:
|
|
78
|
+
print(" FAIL: Failed to spin up/stake service")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
print(" OK: Service deployed and staked!")
|
|
82
|
+
print(f" - Agent: {manager.service.agent_address}")
|
|
83
|
+
print(f" - Multisig: {manager.service.multisig_address}")
|
|
84
|
+
|
|
85
|
+
print("\n" + "=" * 60)
|
|
86
|
+
print(" SUCCESS: Service created and staked!")
|
|
87
|
+
print("=" * 60)
|
|
88
|
+
print(f"\nService ID: {service_id}")
|
|
89
|
+
print("Staking Contract: Expert 7 MM (10k OLAS)")
|
|
90
|
+
print(f"Contract Address: {staking_address}")
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
try:
|
|
96
|
+
success = main()
|
|
97
|
+
sys.exit(0 if success else 1)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"\n❌ Error: {e}")
|
|
100
|
+
import traceback
|
|
101
|
+
|
|
102
|
+
traceback.print_exc()
|
|
103
|
+
sys.exit(1)
|
tools/verify_drain.py
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
"""Verification script for draining services."""
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import subprocess # nosec: B404
|
|
5
4
|
import sys
|
|
6
5
|
import time
|
|
7
6
|
from typing import List
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
8
|
+
from loguru import logger
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
def run_command(command: List[str]): # noqa: D103
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|