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,259 @@
1
+ #!/usr/bin/env python3
2
+ """Integration test: Create service, send legacy mech request, send marketplace mech request.
3
+
4
+ This script tests the full mech request flow on a Tenderly fork:
5
+ 1. Creates a new OLAS service (or uses existing one)
6
+ 2. Spins up the service
7
+ 3. Sends a legacy mech request and verifies the Request event
8
+ 4. Sends a marketplace mech request and verifies the MarketplaceRequest event
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ # Add src to path (scripts are in src/iwa/plugins/olas/scripts/)
15
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent))
16
+
17
+ from iwa.core.chain import ChainInterfaces
18
+ from iwa.core.wallet import Wallet
19
+ from iwa.plugins.olas.constants import OLAS_CONTRACTS
20
+ from iwa.plugins.olas.service_manager import ServiceManager
21
+
22
+
23
+ def print_step(step: str, emoji: str = "🔵"):
24
+ """Print a step with formatting."""
25
+ print(f"\n{emoji} {step}")
26
+ print("-" * 60)
27
+
28
+
29
+ def print_success(msg: str):
30
+ """Print success message."""
31
+ print(f" ✅ {msg}")
32
+
33
+
34
+ def print_info(msg: str):
35
+ """Print info message."""
36
+ print(f" ℹ️ {msg}")
37
+
38
+
39
+ def print_error(msg: str):
40
+ """Print error message."""
41
+ print(f" ❌ {msg}")
42
+
43
+
44
+ def verify_mech_event(chain, tx_hash, contract, expected_event_name, multisig_address):
45
+ """Verify mech request event in transaction receipt."""
46
+ print_info("Fetching transaction receipt...")
47
+ receipt = chain.web3.eth.wait_for_transaction_receipt(tx_hash)
48
+
49
+ print_info(f"Extracting events (expecting '{expected_event_name}')...")
50
+ events = contract.extract_events(receipt)
51
+
52
+ request_event = next((e for e in events if e["name"] == expected_event_name), None)
53
+
54
+ if request_event:
55
+ print_success(f"Found '{expected_event_name}' event!")
56
+ print_info(f"Event Args: {request_event['args']}")
57
+ args = request_event["args"]
58
+
59
+ # Verify requester/sender matches multisig
60
+ requester_key = "requester" if expected_event_name == "MarketplaceRequest" else "sender"
61
+ event_requester = args.get(requester_key)
62
+ if event_requester and event_requester.lower() == multisig_address.lower():
63
+ print_success(f"Event {requester_key} matches multisig address")
64
+ else:
65
+ print_error(
66
+ f"Event {requester_key} ({event_requester}) does not match multisig ({multisig_address})"
67
+ )
68
+ return False
69
+ return True
70
+ else:
71
+ print_error(f"'{expected_event_name}' event not found in transaction logs")
72
+ print_info(f"Found events: {[e['name'] for e in events]}")
73
+ return False
74
+
75
+
76
+ def main(): # noqa: C901
77
+ """Run full mech flow integration test: create -> spin_up -> legacy request -> marketplace request."""
78
+ print("=" * 60)
79
+ print(" OLAS Mech Request Integration Test")
80
+ print("=" * 60)
81
+
82
+ # Initialize wallet
83
+ print_step("Initializing Wallet", "🔐")
84
+ wallet = Wallet()
85
+ print_success(f"Master account: {wallet.master_account.address}")
86
+
87
+ chain = ChainInterfaces().gnosis
88
+
89
+ # Check master balance
90
+ master_balance = chain.get_native_balance_eth(wallet.master_account.address)
91
+ print_info(f"Master xDAI Balance: {master_balance}")
92
+
93
+ # Step 1: Create/Load Service
94
+ print_step("Step 1: Create Service", "1️⃣")
95
+ manager = ServiceManager(wallet)
96
+
97
+ # Check if service already exists
98
+ if manager.service and manager.service.multisig_address:
99
+ print_info(f"Using existing service ID: {manager.service.service_id}")
100
+ print_info(f"Multisig: {manager.service.multisig_address}")
101
+ else:
102
+ service_id = manager.create(
103
+ chain_name="gnosis",
104
+ service_name="mech_test_service",
105
+ )
106
+
107
+ if not service_id:
108
+ print_error("Failed to create service")
109
+ return False
110
+
111
+ print_success(f"Service created with ID: {service_id}")
112
+
113
+ # Step 2: Spin up Service
114
+ print_step("Step 2: Spin up Service", "2️⃣")
115
+ success = manager.spin_up()
116
+ if not success:
117
+ print_error("Failed to spin up service")
118
+ return False
119
+
120
+ print_success("Service deployed!")
121
+
122
+ print_info(f"Service ID: {manager.service.service_id}")
123
+ print_info(f"Agent: {manager.service.agent_address}")
124
+ print_info(f"Multisig: {manager.service.multisig_address}")
125
+
126
+ multisig_address = manager.service.multisig_address
127
+ agent_address = manager.service.agent_address
128
+
129
+ # Fund multisig and agent if needed
130
+ multisig_balance = chain.get_native_balance_eth(multisig_address)
131
+ agent_balance = chain.get_native_balance_eth(agent_address)
132
+ print_info(f"Multisig xDAI Balance: {multisig_balance}")
133
+ print_info(f"Agent xDAI Balance: {agent_balance}")
134
+
135
+ required_payment = 0.05
136
+ if float(multisig_balance) < required_payment:
137
+ print_step("Funding Multisig", "💰")
138
+ success, tx = chain.send_native_transfer(
139
+ from_address=wallet.master_account.address,
140
+ to_address=multisig_address,
141
+ value_wei=int(required_payment * 2 * 1e18),
142
+ sign_callback=lambda tx: wallet.key_storage.sign_transaction(
143
+ tx, wallet.master_account.address
144
+ ),
145
+ )
146
+ if success:
147
+ print_success(f"Funded multisig: {tx}")
148
+ else:
149
+ print_error("Failed to fund multisig")
150
+ return False
151
+
152
+ required_gas = 0.1
153
+ if float(agent_balance) < required_gas:
154
+ print_step("Funding Agent", "⛽")
155
+ success, tx = chain.send_native_transfer(
156
+ from_address=wallet.master_account.address,
157
+ to_address=agent_address,
158
+ value_wei=int(required_gas * 1e18),
159
+ sign_callback=lambda tx: wallet.key_storage.sign_transaction(
160
+ tx, wallet.master_account.address
161
+ ),
162
+ )
163
+ if success:
164
+ print_success(f"Funded agent: {tx}")
165
+ else:
166
+ print_error("Failed to fund agent")
167
+ return False
168
+
169
+ # Dummy request data
170
+ dummy_data = b"test_request_data_12345"
171
+ payment_wei = int(0.01 * 1e18)
172
+
173
+ # Get contract addresses
174
+ protocol_contracts = OLAS_CONTRACTS.get("gnosis", {})
175
+ legacy_mech_address = protocol_contracts.get("OLAS_MECH")
176
+ marketplace_address = protocol_contracts.get("OLAS_MECH_MARKETPLACE")
177
+
178
+ # Step 3: Send Legacy Mech Request
179
+ print_step("Step 3: Send Legacy Mech Request", "3️⃣")
180
+ from iwa.plugins.olas.contracts.mech import MechContract
181
+
182
+ tx_hash_legacy = manager.send_mech_request(
183
+ data=dummy_data,
184
+ value=payment_wei,
185
+ use_marketplace=False,
186
+ mech_address=str(legacy_mech_address),
187
+ )
188
+
189
+ if not tx_hash_legacy:
190
+ print_error("Failed to send legacy mech request")
191
+ return False
192
+
193
+ print_success(f"Legacy mech request sent: {tx_hash_legacy}")
194
+
195
+ # Verify legacy event
196
+ print_step("Step 3b: Verify Legacy Mech Event", "✅")
197
+ legacy_mech = MechContract(str(legacy_mech_address), chain_name="gnosis")
198
+ if not verify_mech_event(chain, tx_hash_legacy, legacy_mech, "Request", multisig_address):
199
+ print_error("Legacy mech event verification failed")
200
+ return False
201
+
202
+ print_success("Legacy mech request verified!")
203
+
204
+ # Step 4: Send Marketplace Mech Request
205
+ print_step("Step 4: Send Marketplace Mech Request", "4️⃣")
206
+ from iwa.plugins.olas.contracts.mech_marketplace import MechMarketplaceContract
207
+
208
+ # Known registered mech on Gnosis marketplace
209
+ priority_mech = "0x601024E27f1C67B28209E24272CED8A31fc8151F"
210
+
211
+ # API uses smart defaults:
212
+ # - max_delivery_rate defaults to value
213
+ # - payment_type defaults to NATIVE
214
+ tx_hash_marketplace = manager.send_mech_request(
215
+ data=dummy_data,
216
+ value=payment_wei,
217
+ use_marketplace=True,
218
+ priority_mech=priority_mech,
219
+ )
220
+
221
+ if not tx_hash_marketplace:
222
+ print_error("Failed to send marketplace mech request")
223
+ return False
224
+
225
+ print_success(f"Marketplace mech request sent: {tx_hash_marketplace}")
226
+
227
+ # Verify marketplace event
228
+ print_step("Step 4b: Verify Marketplace Mech Event", "✅")
229
+ marketplace = MechMarketplaceContract(str(marketplace_address), chain_name="gnosis")
230
+ if not verify_mech_event(
231
+ chain, tx_hash_marketplace, marketplace, "MarketplaceRequest", multisig_address
232
+ ):
233
+ print_error("Marketplace mech event verification failed")
234
+ return False
235
+
236
+ print_success("Marketplace mech request verified!")
237
+
238
+ # Summary
239
+ print("\n" + "=" * 60)
240
+ print(" 🎉 All Mech Requests Completed Successfully!")
241
+ print("=" * 60)
242
+ print(f" Service ID: {manager.service.service_id}")
243
+ print(f" Legacy Mech Tx: {tx_hash_legacy}")
244
+ print(f" Marketplace Mech Tx: {tx_hash_marketplace}")
245
+ print("=" * 60)
246
+
247
+ return True
248
+
249
+
250
+ if __name__ == "__main__":
251
+ try:
252
+ success = main()
253
+ sys.exit(0 if success else 1)
254
+ except Exception as e:
255
+ print(f"\n❌ Error: {e}")
256
+ import traceback
257
+
258
+ traceback.print_exc()
259
+ sys.exit(1)
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """Simple lifecycle test: create → spin_up → wind_down (no staking)."""
3
+
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ # Add src to path (scripts are in src/iwa/plugins/olas/scripts/)
8
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent.parent))
9
+
10
+ from iwa.core.wallet import Wallet
11
+ from iwa.plugins.olas.service_manager import ServiceManager
12
+
13
+
14
+ def main():
15
+ """Run simple lifecycle integration test: create -> spin_up."""
16
+ print("=" * 60)
17
+ print(" Simple Lifecycle Test: create → spin_up → wind_down")
18
+ print("=" * 60)
19
+
20
+ # Initialize wallet
21
+ print("\n🔐 Initializing Wallet...")
22
+ wallet = Wallet()
23
+ print(f" ✅ Master account: {wallet.master_account.address}")
24
+
25
+ # Create service
26
+ print("\n1️⃣ Creating Service...")
27
+ manager = ServiceManager(wallet)
28
+ service_id = manager.create(
29
+ chain_name="gnosis",
30
+ service_name="simple_test_service",
31
+ )
32
+
33
+ if not service_id:
34
+ print(" ❌ Failed to create service")
35
+ return False
36
+
37
+ print(f" ✅ Service created with ID: {service_id}")
38
+
39
+ # Spin up (activate → register → deploy)
40
+ print("\n2️⃣ Spinning up Service...")
41
+ success = manager.spin_up()
42
+ if not success:
43
+ print(" ❌ Failed to spin up service")
44
+ return False
45
+
46
+ print(" ✅ Service deployed!")
47
+ print(f" - Agent: {manager.service.agent_address}")
48
+ print(f" - Multisig: {manager.service.multisig_address}")
49
+
50
+ # Wind down (terminate → unbond)
51
+ # print("\n3️⃣ Winding down Service...")
52
+ # success = manager.wind_down()
53
+ # if not success:
54
+ # print(" ❌ Failed to wind down service")
55
+ # return False
56
+
57
+ # print(" ✅ Service wound down successfully!")
58
+
59
+ print("\n" + "=" * 60)
60
+ print(" 🎉 Full lifecycle completed!")
61
+ print("=" * 60)
62
+ return True
63
+
64
+
65
+ if __name__ == "__main__":
66
+ try:
67
+ success = main()
68
+ sys.exit(0 if success else 1)
69
+ except Exception as e:
70
+ print(f"\n❌ Error: {e}")
71
+ import traceback
72
+
73
+ traceback.print_exc()
74
+ sys.exit(1)
@@ -0,0 +1,60 @@
1
+ """Service Manager package for OLAS services."""
2
+
3
+ from iwa.core.chain import ChainInterfaces
4
+ from iwa.core.contracts.erc20 import ERC20Contract
5
+ from iwa.core.models import Config
6
+ from iwa.plugins.olas.constants import OLAS_CONTRACTS
7
+ from iwa.plugins.olas.contracts.mech import MechContract
8
+ from iwa.plugins.olas.contracts.mech_marketplace import MechMarketplaceContract
9
+ from iwa.plugins.olas.contracts.service import (
10
+ ServiceManagerContract,
11
+ ServiceRegistryContract,
12
+ ServiceState,
13
+ )
14
+ from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
15
+ from iwa.plugins.olas.models import OlasConfig, Service, StakingStatus
16
+ from iwa.plugins.olas.service_manager.base import ServiceManagerBase
17
+ from iwa.plugins.olas.service_manager.drain import DrainManagerMixin
18
+ from iwa.plugins.olas.service_manager.lifecycle import LifecycleManagerMixin
19
+ from iwa.plugins.olas.service_manager.mech import MechManagerMixin
20
+ from iwa.plugins.olas.service_manager.staking import StakingManagerMixin
21
+
22
+
23
+ class ServiceManager(
24
+ LifecycleManagerMixin,
25
+ DrainManagerMixin,
26
+ MechManagerMixin,
27
+ StakingManagerMixin,
28
+ ServiceManagerBase,
29
+ ):
30
+ """ServiceManager for OLAS services with multi-service support.
31
+
32
+ Combines functionality from:
33
+ - LifecycleManagerMixin: create, deploy, terminate, etc.
34
+ - StakingManagerMixin: stake, unstake, checkpoint
35
+ - DrainManagerMixin: drain, claim_rewards
36
+ - MechManagerMixin: send_mech_request
37
+ - ServiceManagerBase: init, common config
38
+ """
39
+
40
+ pass
41
+
42
+
43
+ __all__ = [
44
+ "ServiceManager",
45
+ # Re-export commonly used types for backward compatibility
46
+ "Service",
47
+ "StakingStatus",
48
+ "OlasConfig",
49
+ "ServiceState",
50
+ "StakingState",
51
+ "StakingContract",
52
+ "ServiceRegistryContract",
53
+ "ServiceManagerContract",
54
+ "MechContract",
55
+ "MechMarketplaceContract",
56
+ "ERC20Contract",
57
+ "ChainInterfaces",
58
+ "Config",
59
+ "OLAS_CONTRACTS",
60
+ ]
@@ -0,0 +1,113 @@
1
+ """ServiceManager base class."""
2
+
3
+ from typing import Dict, Optional
4
+
5
+ from loguru import logger
6
+
7
+ from iwa.core.chain import ChainInterfaces
8
+ from iwa.core.models import Config
9
+ from iwa.core.wallet import Wallet
10
+ from iwa.plugins.olas.constants import OLAS_CONTRACTS
11
+ from iwa.plugins.olas.contracts.service import ServiceManagerContract, ServiceRegistryContract
12
+ from iwa.plugins.olas.models import OlasConfig
13
+
14
+
15
+ class ServiceManagerBase:
16
+ """Base class for ServiceManager."""
17
+
18
+ def __init__(self, wallet: Wallet, service_key: Optional[str] = None):
19
+ """Initialize ServiceManager.
20
+
21
+ Args:
22
+ wallet: The wallet instance for signing transactions.
23
+ service_key: Optional key (chain_name:service_id) to select a specific service.
24
+ If not provided, service operations require explicit service selection.
25
+
26
+ """
27
+ self.wallet = wallet
28
+ self.global_config = Config()
29
+
30
+ self.olas_config = self.global_config.plugins.get("olas")
31
+ if isinstance(self.olas_config, dict):
32
+ self.olas_config = OlasConfig(**self.olas_config)
33
+ self.global_config.plugins["olas"] = self.olas_config
34
+ elif self.olas_config is None:
35
+ self.olas_config = OlasConfig()
36
+ self.global_config.plugins["olas"] = self.olas_config
37
+
38
+ # Get service by key if provided
39
+ self.service = None
40
+ if service_key and ":" in service_key:
41
+ chain_name, service_id = service_key.split(":", 1)
42
+ self.service = self.olas_config.get_service(chain_name, int(service_id))
43
+
44
+ # Initialize contracts (default to gnosis)
45
+ service_chain = getattr(self.service, "chain_name", "gnosis")
46
+ chain_name = service_chain if isinstance(service_chain, str) else "gnosis"
47
+ self._init_contracts(chain_name)
48
+
49
+ # Initialize TransferService from wallet
50
+ self.transfer_service = self.wallet.transfer_service
51
+
52
+ def _init_contracts(self, chain_name: str) -> None:
53
+ """Initialize contracts for the given chain."""
54
+ chain_interface = ChainInterfaces().get(chain_name)
55
+
56
+ # Get protocol contracts from plugin-local constants
57
+ protocol_contracts = OLAS_CONTRACTS.get(chain_name.lower(), {})
58
+ registry_address = protocol_contracts.get("OLAS_SERVICE_REGISTRY")
59
+ manager_address = protocol_contracts.get("OLAS_SERVICE_MANAGER")
60
+
61
+ if not registry_address or not manager_address:
62
+ raise ValueError(f"OLAS contracts not found for chain: {chain_name}")
63
+
64
+ self.registry = ServiceRegistryContract(registry_address, chain_name=chain_name)
65
+ self.manager = ServiceManagerContract(manager_address, chain_name=chain_name)
66
+ logger.info(f"[SM-INIT] ServiceManager initialized. Chain: {chain_name}")
67
+ logger.info(f"[SM-INIT] Registry Address: {self.registry.address}")
68
+ logger.info(f"[SM-INIT] Manager Address: {self.manager.address}")
69
+ self.chain_interface = chain_interface
70
+ self.chain_name = chain_name.lower()
71
+
72
+ def _save_config(self) -> None:
73
+ """Persist configuration to config.yaml."""
74
+ self.global_config.save_config()
75
+
76
+ def _update_and_save_service_state(self) -> None:
77
+ """Update the service object in olas_config and persist to config.yaml."""
78
+ if self.service:
79
+ # Update the service object in the configuration dictionary
80
+ # This ensures that changes to self.service (which comes from the Router)
81
+ # are reflected in the ServiceManager's internal configuration state
82
+ # before saving to disk.
83
+ self.olas_config.add_service(self.service)
84
+ self._save_config()
85
+
86
+ def get(self) -> Optional[Dict]:
87
+ """Get service details by ID."""
88
+ if not self.service:
89
+ logger.error("No active service")
90
+ return None
91
+ return self.registry.get_service(self.service.service_id)
92
+
93
+ def get_service_state(self, service_id: Optional[int] = None) -> str:
94
+ """Get the state of a service as a string.
95
+
96
+ Args:
97
+ service_id: Optional service ID. If not provided, uses the active service.
98
+
99
+ Returns:
100
+ The state name (e.g., 'DEPLOYED') or 'UNKNOWN' if not found.
101
+
102
+ """
103
+ if service_id is None:
104
+ if not self.service:
105
+ return "UNKNOWN"
106
+ service_id = self.service.service_id
107
+
108
+ try:
109
+ info = self.registry.get_service(service_id)
110
+ return info["state"].name
111
+ except Exception as e:
112
+ logger.debug(f"Failed to get service state for {service_id}: {e}")
113
+ return "UNKNOWN"