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.
Files changed (58) hide show
  1. iwa/core/chain/interface.py +51 -30
  2. iwa/core/chain/models.py +9 -15
  3. iwa/core/contracts/contract.py +8 -2
  4. iwa/core/pricing.py +10 -8
  5. iwa/core/services/safe.py +13 -8
  6. iwa/core/services/transaction.py +211 -7
  7. iwa/core/utils.py +22 -0
  8. iwa/core/wallet.py +2 -1
  9. iwa/plugins/gnosis/safe.py +4 -3
  10. iwa/plugins/gnosis/tests/test_safe.py +9 -7
  11. iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +926 -0
  12. iwa/plugins/olas/contracts/service.py +54 -4
  13. iwa/plugins/olas/contracts/staking.py +2 -3
  14. iwa/plugins/olas/plugin.py +14 -7
  15. iwa/plugins/olas/service_manager/lifecycle.py +382 -85
  16. iwa/plugins/olas/service_manager/mech.py +1 -1
  17. iwa/plugins/olas/service_manager/staking.py +229 -82
  18. iwa/plugins/olas/tests/test_olas_contracts.py +6 -2
  19. iwa/plugins/olas/tests/test_plugin.py +6 -1
  20. iwa/plugins/olas/tests/test_plugin_full.py +12 -7
  21. iwa/plugins/olas/tests/test_service_lifecycle.py +1 -4
  22. iwa/plugins/olas/tests/test_service_manager.py +59 -89
  23. iwa/plugins/olas/tests/test_service_manager_errors.py +1 -2
  24. iwa/plugins/olas/tests/test_service_manager_flows.py +5 -15
  25. iwa/plugins/olas/tests/test_service_manager_validation.py +16 -15
  26. iwa/tools/list_contracts.py +2 -2
  27. iwa/web/dependencies.py +1 -3
  28. iwa/web/routers/accounts.py +1 -2
  29. iwa/web/routers/olas/admin.py +1 -3
  30. iwa/web/routers/olas/funding.py +1 -3
  31. iwa/web/routers/olas/general.py +1 -3
  32. iwa/web/routers/olas/services.py +53 -21
  33. iwa/web/routers/olas/staking.py +27 -24
  34. iwa/web/routers/swap.py +1 -2
  35. iwa/web/routers/transactions.py +0 -2
  36. iwa/web/server.py +8 -6
  37. iwa/web/static/app.js +22 -0
  38. iwa/web/tests/test_web_endpoints.py +1 -1
  39. iwa/web/tests/test_web_olas.py +1 -1
  40. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/METADATA +1 -1
  41. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/RECORD +58 -56
  42. tests/test_chain.py +12 -7
  43. tests/test_chain_interface_coverage.py +3 -2
  44. tests/test_contract.py +165 -0
  45. tests/test_keys.py +2 -1
  46. tests/test_legacy_wallet.py +11 -0
  47. tests/test_pricing.py +32 -15
  48. tests/test_safe_coverage.py +3 -3
  49. tests/test_safe_service.py +3 -6
  50. tests/test_service_transaction.py +8 -3
  51. tests/test_staking_router.py +6 -3
  52. tests/test_transaction_service.py +4 -0
  53. tools/create_and_stake_service.py +103 -0
  54. tools/verify_drain.py +1 -4
  55. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/WHEEL +0 -0
  56. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/entry_points.txt +0 -0
  57. {iwa-0.0.2.dist-info → iwa-0.0.11.dist-info}/licenses/LICENSE +0 -0
  58. {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 == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # gitleaks:allow
70
+ private_key
71
+ == "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" # gitleaks:allow
71
72
  ):
72
73
  addr = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
73
74
  # 2. Handle our mock key format
@@ -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
- price_service.cache["ethereum_eur"] = {"price": 100.0, "timestamp": datetime.now()}
46
+ cache_data = {
47
+ "ethereum_eur": {"price": 100.0, "timestamp": datetime.now()}
48
+ }
40
49
 
41
- with patch("iwa.core.pricing.requests.get") as mock_get:
42
- price = price_service.get_token_price("ethereum", "eur")
43
- assert price == 100.0
44
- mock_get.assert_not_called()
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
- price_service.cache["ethereum_eur"] = {
52
- "price": 100.0,
53
- "timestamp": datetime.now() - timedelta(minutes=10),
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.requests.get") as mock_get:
57
- mock_get.return_value.status_code = 200
58
- mock_get.return_value.json.return_value = {"ethereum": {"eur": 200.0}}
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
- price = price_service.get_token_price("ethereum", "eur")
61
- assert price == 200.0
62
- mock_get.assert_called_once()
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
@@ -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 = "0xTxHash"
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 = "0xTxHash"
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.services.safe.secrets") as mock_settings:
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:
@@ -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 = "0xTxHash"
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 = "0xTxHashSalted"
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 3 times (max_retries)
119
- assert chain_interface.web3.eth.send_raw_transaction.call_count == 3
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
- success, receipt = transaction_service.sign_and_send(tx, "unknown_signer")
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
@@ -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
- mock_w3 = MagicMock()
23
- mock_w3.eth.contract.side_effect = Exception("Contract error")
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
- # Configure logging
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