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
|
@@ -296,7 +296,7 @@ class MechManagerMixin:
|
|
|
296
296
|
chain_name=self.chain_name,
|
|
297
297
|
tags=["olas_mech_request"],
|
|
298
298
|
)
|
|
299
|
-
tx_hash = receipt.get("transactionHash")
|
|
299
|
+
tx_hash = Web3.to_hex(receipt.get("transactionHash")) if success else None
|
|
300
300
|
|
|
301
301
|
if not tx_hash:
|
|
302
302
|
logger.error("Failed to send mech request transaction")
|
|
@@ -1,4 +1,48 @@
|
|
|
1
|
-
"""Staking manager mixin.
|
|
1
|
+
"""Staking manager mixin for OLAS service staking operations.
|
|
2
|
+
|
|
3
|
+
OLAS Token Flow Overview
|
|
4
|
+
========================
|
|
5
|
+
|
|
6
|
+
For OLAS token-bonded services (e.g., Expert 7 MM requiring 10,000 OLAS total),
|
|
7
|
+
the tokens flow through multiple stages:
|
|
8
|
+
|
|
9
|
+
1. CREATE SERVICE
|
|
10
|
+
- Service is registered on-chain with bond parameters
|
|
11
|
+
- Service Owner approves Token Utility to spend OLAS (2 × bond)
|
|
12
|
+
- NO OLAS tokens move yet
|
|
13
|
+
|
|
14
|
+
2. ACTIVATION (min_staking_deposit = 5,000 OLAS for 10k contract)
|
|
15
|
+
- Service Owner approves Token Utility for the security deposit
|
|
16
|
+
- TX sends 1 wei native value (not 5k OLAS!)
|
|
17
|
+
- Token Utility internally calls transferFrom() to move 5k OLAS
|
|
18
|
+
- 5k OLAS moves: Service Owner → Token Utility
|
|
19
|
+
|
|
20
|
+
3. REGISTRATION (agent_bond = 5,000 OLAS for 10k contract)
|
|
21
|
+
- Service Owner approves Token Utility for the bond amount
|
|
22
|
+
- TX sends 1 wei native value per agent (not 5k OLAS!)
|
|
23
|
+
- Token Utility internally calls transferFrom() to move 5k OLAS
|
|
24
|
+
- 5k OLAS moves: Service Owner → Token Utility
|
|
25
|
+
|
|
26
|
+
4. DEPLOY
|
|
27
|
+
- Creates the Safe multisig for the service
|
|
28
|
+
- NO OLAS tokens move
|
|
29
|
+
|
|
30
|
+
5. STAKE (this module) ★
|
|
31
|
+
- Only the Service NFT is approved to the staking contract
|
|
32
|
+
- NO OLAS tokens move in this transaction!
|
|
33
|
+
- The staking contract reads the deposited amounts from Token Utility
|
|
34
|
+
- Service Registry L2 token (NFT) moves: Owner → Staking Contract
|
|
35
|
+
|
|
36
|
+
Key Insight:
|
|
37
|
+
At stake time, the Service Owner's OLAS balance is 0 (all 10k was deposited
|
|
38
|
+
during activation + registration). This is correct! The staking contract
|
|
39
|
+
pulls position data from the Token Utility, not from the owner's wallet.
|
|
40
|
+
|
|
41
|
+
Contract Addresses (Gnosis):
|
|
42
|
+
- Token Utility: 0xa45E...8eD8
|
|
43
|
+
- Service Registry L2: 0x9338...55fD
|
|
44
|
+
- OLAS Token: 0xcE11...d9f
|
|
45
|
+
"""
|
|
2
46
|
|
|
3
47
|
from datetime import datetime, timezone
|
|
4
48
|
from typing import Optional
|
|
@@ -6,14 +50,28 @@ from typing import Optional
|
|
|
6
50
|
from loguru import logger
|
|
7
51
|
from web3 import Web3
|
|
8
52
|
|
|
9
|
-
from iwa.core.contracts.erc20 import ERC20Contract
|
|
10
53
|
from iwa.core.types import EthereumAddress
|
|
54
|
+
from iwa.core.utils import get_tx_hash
|
|
11
55
|
from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
|
|
12
56
|
from iwa.plugins.olas.models import StakingStatus
|
|
13
57
|
|
|
14
58
|
|
|
15
59
|
class StakingManagerMixin:
|
|
16
|
-
"""Mixin for staking operations.
|
|
60
|
+
"""Mixin for staking operations on OLAS services.
|
|
61
|
+
|
|
62
|
+
This mixin handles the final step of the service lifecycle: staking a
|
|
63
|
+
deployed service into a staking contract to earn OLAS rewards.
|
|
64
|
+
|
|
65
|
+
Important: By the time stake() is called, all OLAS tokens have already
|
|
66
|
+
been deposited to the Token Utility during activation and registration.
|
|
67
|
+
The stake transaction only transfers the Service NFT, not OLAS tokens.
|
|
68
|
+
|
|
69
|
+
Staking Requirements:
|
|
70
|
+
- Service must be in DEPLOYED state
|
|
71
|
+
- Service must be created with OLAS token (not native currency)
|
|
72
|
+
- Staking contract must have available slots
|
|
73
|
+
- Service token must match staking contract's required token
|
|
74
|
+
"""
|
|
17
75
|
|
|
18
76
|
def get_staking_status(self) -> Optional[StakingStatus]:
|
|
19
77
|
"""Get comprehensive staking status for the active service.
|
|
@@ -153,20 +211,28 @@ class StakingManagerMixin:
|
|
|
153
211
|
def stake(self, staking_contract) -> bool:
|
|
154
212
|
"""Stake the service in a staking contract.
|
|
155
213
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
214
|
+
This is the final step after create → activate → register → deploy.
|
|
215
|
+
At this point, all OLAS tokens are already in the Token Utility.
|
|
216
|
+
|
|
217
|
+
Token Flow at Stake Time:
|
|
218
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
219
|
+
│ What Moves: │
|
|
220
|
+
│ • Service NFT (ERC-721): Owner → Staking Contract │
|
|
221
|
+
│ │
|
|
222
|
+
│ What does NOT Move: │
|
|
223
|
+
│ • OLAS tokens - already in Token Utility from earlier steps │
|
|
224
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
225
|
+
|
|
226
|
+
Why no OLAS transfer?
|
|
227
|
+
The staking contract reads the service's bond/deposit from the
|
|
228
|
+
Token Utility contract. It doesn't need a new transfer - it just
|
|
229
|
+
verifies the amounts are sufficient and locks the service.
|
|
230
|
+
|
|
231
|
+
Process:
|
|
232
|
+
1. Validate requirements (state, token, slots)
|
|
233
|
+
2. Approve Service NFT to staking contract
|
|
234
|
+
3. Call stake(serviceId) on staking contract
|
|
235
|
+
4. Verify ServiceStaked event
|
|
170
236
|
|
|
171
237
|
Args:
|
|
172
238
|
staking_contract: StakingContract instance to stake in.
|
|
@@ -175,92 +241,172 @@ class StakingManagerMixin:
|
|
|
175
241
|
True if staking succeeded, False otherwise.
|
|
176
242
|
|
|
177
243
|
"""
|
|
244
|
+
logger.info("=" * 50)
|
|
245
|
+
logger.info(f"[STAKE] Starting staking for service {self.service.service_id}")
|
|
246
|
+
logger.info(f"[STAKE] Contract: {staking_contract.address}")
|
|
247
|
+
logger.info("=" * 50)
|
|
248
|
+
|
|
178
249
|
# 1. Validation
|
|
250
|
+
logger.info("[STAKE] Step 1: Checking requirements...")
|
|
179
251
|
requirements = self._check_stake_requirements(staking_contract)
|
|
180
252
|
if not requirements:
|
|
253
|
+
logger.error("[STAKE] Step 1 FAILED: Requirements not met")
|
|
181
254
|
return False
|
|
255
|
+
logger.info("[STAKE] Step 1 OK: All requirements met")
|
|
182
256
|
|
|
183
257
|
min_deposit = requirements["min_deposit"]
|
|
258
|
+
logger.info(
|
|
259
|
+
f"[STAKE] Min deposit required: {min_deposit} wei ({min_deposit / 1e18:.2f} OLAS)"
|
|
260
|
+
)
|
|
184
261
|
|
|
185
|
-
# 2. Approve
|
|
186
|
-
|
|
262
|
+
# 2. Approve Service NFT
|
|
263
|
+
logger.info("[STAKE] Step 2: Approving service NFT...")
|
|
264
|
+
if not self._approve_staking_tokens(staking_contract):
|
|
265
|
+
logger.error("[STAKE] Step 2 FAILED: NFT approval failed")
|
|
187
266
|
return False
|
|
267
|
+
logger.info("[STAKE] Step 2 OK: Service NFT approved")
|
|
188
268
|
|
|
189
269
|
# 3. Execute Stake Transaction
|
|
190
|
-
|
|
270
|
+
logger.info("[STAKE] Step 3: Executing stake transaction...")
|
|
271
|
+
result = self._execute_stake_transaction(staking_contract)
|
|
272
|
+
if result:
|
|
273
|
+
logger.info("[STAKE] Step 3 OK: Staking successful")
|
|
274
|
+
logger.info("=" * 50)
|
|
275
|
+
logger.info(f"[STAKE] COMPLETE - Service {self.service.service_id} is now staked")
|
|
276
|
+
logger.info("=" * 50)
|
|
277
|
+
else:
|
|
278
|
+
logger.error("[STAKE] Step 3 FAILED: Stake transaction failed")
|
|
279
|
+
return result
|
|
191
280
|
|
|
192
281
|
def _check_stake_requirements(self, staking_contract) -> Optional[dict]:
|
|
193
|
-
"""Validate all conditions required for staking.
|
|
282
|
+
"""Validate all conditions required for staking.
|
|
283
|
+
|
|
284
|
+
Checks performed:
|
|
285
|
+
1. Service State: Must be DEPLOYED (multisig created)
|
|
286
|
+
2. Token Match: Service token == Staking contract's staking_token
|
|
287
|
+
3. Agent Bond: Logged (may show 1 wei on-chain, this is normal)
|
|
288
|
+
4. Available Slots: Contract must have free slots
|
|
289
|
+
|
|
290
|
+
Note on OLAS Balance:
|
|
291
|
+
We do NOT check owner's OLAS balance here. By this point:
|
|
292
|
+
- 5k OLAS was transferred during activation (to Token Utility)
|
|
293
|
+
- 5k OLAS was transferred during registration (to Token Utility)
|
|
294
|
+
- Owner's OLAS balance is 0, and that's correct!
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
staking_contract: StakingContract to validate against.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Dict with {min_deposit, staking_token} if valid, None otherwise.
|
|
301
|
+
|
|
302
|
+
"""
|
|
194
303
|
from iwa.plugins.olas.contracts.service import ServiceState
|
|
195
304
|
|
|
196
|
-
|
|
305
|
+
logger.debug("[STAKE] Fetching contract requirements...")
|
|
197
306
|
reqs = staking_contract.get_requirements()
|
|
198
307
|
min_deposit = reqs["min_staking_deposit"]
|
|
199
308
|
required_bond = reqs["required_agent_bond"]
|
|
200
309
|
staking_token = Web3.to_checksum_address(reqs["staking_token"])
|
|
201
310
|
staking_token_lower = staking_token.lower()
|
|
202
311
|
|
|
203
|
-
logger.info(
|
|
312
|
+
logger.info("[STAKE] Contract requirements:")
|
|
313
|
+
logger.info(f"[STAKE] - min_staking_deposit: {min_deposit} wei")
|
|
314
|
+
logger.info(f"[STAKE] - required_agent_bond: {required_bond} wei")
|
|
315
|
+
logger.info(f"[STAKE] - staking_token: {staking_token}")
|
|
204
316
|
|
|
205
317
|
# Check service state
|
|
318
|
+
logger.debug("[STAKE] Checking service state...")
|
|
206
319
|
service_info = self.registry.get_service(self.service.service_id)
|
|
207
320
|
service_state = service_info["state"]
|
|
208
|
-
logger.info(f"Service state: {service_state.name}")
|
|
321
|
+
logger.info(f"[STAKE] Service state: {service_state.name}")
|
|
209
322
|
|
|
210
323
|
if service_state != ServiceState.DEPLOYED:
|
|
211
|
-
logger.error("Service is
|
|
324
|
+
logger.error(f"[STAKE] FAIL: Service is {service_state.name}, expected DEPLOYED")
|
|
212
325
|
return None
|
|
326
|
+
logger.debug("[STAKE] OK: Service is DEPLOYED")
|
|
213
327
|
|
|
214
328
|
# Check token compatibility
|
|
215
329
|
service_token = (self.service.token_address or "").lower()
|
|
330
|
+
logger.debug(f"[STAKE] Service token: {service_token}")
|
|
216
331
|
if service_token != staking_token_lower:
|
|
217
332
|
logger.error(
|
|
218
|
-
f"
|
|
219
|
-
f"
|
|
333
|
+
f"[STAKE] FAIL: Token mismatch - service={service_token or 'native'}, "
|
|
334
|
+
f"contract requires={staking_token_lower}"
|
|
220
335
|
)
|
|
221
336
|
return None
|
|
337
|
+
logger.debug("[STAKE] OK: Token matches")
|
|
222
338
|
|
|
223
339
|
# Check agent bond
|
|
340
|
+
# NOTE: On-chain bond values often show 1 wei regardless of what was passed
|
|
341
|
+
# during service creation. This is a known issue with the OLAS contracts.
|
|
342
|
+
# We log a warning but don't block staking because of this discrepancy.
|
|
343
|
+
logger.debug("[STAKE] Checking agent bond...")
|
|
224
344
|
try:
|
|
225
345
|
agent_ids = service_info["agent_ids"]
|
|
226
346
|
if not agent_ids:
|
|
227
|
-
logger.error("No agent IDs found
|
|
347
|
+
logger.error("[STAKE] FAIL: No agent IDs found")
|
|
228
348
|
return None
|
|
229
349
|
|
|
230
|
-
|
|
231
|
-
agent_params =
|
|
350
|
+
params_list = self.registry.get_agent_params(self.service.service_id)
|
|
351
|
+
agent_params = params_list[0]
|
|
232
352
|
current_bond = agent_params["bond"]
|
|
353
|
+
logger.info(
|
|
354
|
+
f"[STAKE] Agent bond on-chain: {current_bond} wei (required: {required_bond} wei)"
|
|
355
|
+
)
|
|
233
356
|
|
|
234
357
|
if current_bond < required_bond:
|
|
235
|
-
logger.
|
|
236
|
-
f"
|
|
237
|
-
"
|
|
358
|
+
logger.warning(
|
|
359
|
+
f"[STAKE] WARN: On-chain bond ({current_bond}) < required ({required_bond}). "
|
|
360
|
+
"This is a known on-chain reporting issue. Proceeding anyway."
|
|
238
361
|
)
|
|
239
|
-
|
|
362
|
+
else:
|
|
363
|
+
logger.debug("[STAKE] OK: Agent bond sufficient")
|
|
240
364
|
except Exception as e:
|
|
241
|
-
logger.warning(f"Could not verify agent bond: {e}")
|
|
365
|
+
logger.warning(f"[STAKE] WARN: Could not verify agent bond: {e}")
|
|
242
366
|
|
|
243
367
|
# Check free slots
|
|
368
|
+
logger.debug("[STAKE] Checking available slots...")
|
|
244
369
|
staked_count = len(staking_contract.get_service_ids())
|
|
245
370
|
max_services = staking_contract.max_num_services
|
|
371
|
+
free_slots = max_services - staked_count
|
|
372
|
+
logger.info(f"[STAKE] Slots: {staked_count}/{max_services} used, {free_slots} free")
|
|
373
|
+
|
|
246
374
|
if staked_count >= max_services:
|
|
247
|
-
logger.error("
|
|
375
|
+
logger.error("[STAKE] FAIL: No free slots")
|
|
248
376
|
return None
|
|
377
|
+
logger.debug("[STAKE] OK: Slots available")
|
|
249
378
|
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
logger.error(
|
|
255
|
-
f"Not enough tokens to stake service (have {master_balance}, need {min_deposit})"
|
|
256
|
-
)
|
|
257
|
-
return None
|
|
379
|
+
# NOTE: We don't check OLAS balance here because OLAS was already
|
|
380
|
+
# deposited to the Token Utility during activation (min_staking_deposit)
|
|
381
|
+
# and registration (agent_bond). The staking contract pulls from there.
|
|
382
|
+
logger.debug("[STAKE] OLAS already deposited to Token Utility during activation/registration")
|
|
258
383
|
|
|
259
384
|
return {"min_deposit": min_deposit, "staking_token": staking_token}
|
|
260
385
|
|
|
261
|
-
def _approve_staking_tokens(self, staking_contract
|
|
262
|
-
"""Approve
|
|
263
|
-
|
|
386
|
+
def _approve_staking_tokens(self, staking_contract) -> bool:
|
|
387
|
+
"""Approve the Service NFT for transfer to the staking contract.
|
|
388
|
+
|
|
389
|
+
What This Does:
|
|
390
|
+
Calls approve(stakingContract, serviceId) on the Service Registry L2.
|
|
391
|
+
This allows the staking contract to transferFrom the NFT.
|
|
392
|
+
|
|
393
|
+
What This Does NOT Do:
|
|
394
|
+
- Does NOT approve OLAS tokens (they're already in Token Utility)
|
|
395
|
+
- Does NOT transfer any tokens (that happens in _execute_stake_transaction)
|
|
396
|
+
|
|
397
|
+
Token/NFT Movement:
|
|
398
|
+
BEFORE: Owner has NFT, staking contract has no approval
|
|
399
|
+
AFTER: Owner has NFT, staking contract is approved to take it
|
|
400
|
+
|
|
401
|
+
Who Signs:
|
|
402
|
+
Master account (must be service owner)
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
True if approval succeeded, False otherwise.
|
|
406
|
+
|
|
407
|
+
"""
|
|
408
|
+
# Approve service NFT - this is an ERC-721 approval, not ERC-20
|
|
409
|
+
logger.debug("[STAKE] Approving service NFT for staking contract...")
|
|
264
410
|
approve_tx = self.registry.prepare_approve_tx(
|
|
265
411
|
from_address=self.wallet.master_account.address,
|
|
266
412
|
spender=staking_contract.address,
|
|
@@ -275,44 +421,40 @@ class StakingManagerMixin:
|
|
|
275
421
|
)
|
|
276
422
|
|
|
277
423
|
if not success:
|
|
278
|
-
logger.error("
|
|
424
|
+
logger.error("[STAKE] FAIL: Service NFT approval failed")
|
|
279
425
|
return False
|
|
280
426
|
|
|
281
|
-
|
|
427
|
+
tx_hash = get_tx_hash(receipt)
|
|
428
|
+
logger.info(f"[STAKE] Service NFT approved: {tx_hash}")
|
|
429
|
+
return True
|
|
282
430
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
# Retching for simplicity and safety
|
|
286
|
-
reqs = staking_contract.get_requirements()
|
|
287
|
-
staking_token = Web3.to_checksum_address(reqs["staking_token"])
|
|
288
|
-
erc20_contract = ERC20Contract(staking_token)
|
|
431
|
+
def _execute_stake_transaction(self, staking_contract) -> bool:
|
|
432
|
+
"""Execute the actual stake transaction on the staking contract.
|
|
289
433
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
434
|
+
What Happens Internally:
|
|
435
|
+
1. Staking contract calls transferFrom to take the Service NFT
|
|
436
|
+
2. Staking contract reads bond/deposit from Token Utility
|
|
437
|
+
3. Staking contract records the service as staked
|
|
438
|
+
4. ServiceStaked event is emitted
|
|
295
439
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
chain_name=self.chain_name,
|
|
300
|
-
tags=["olas_approve_olas_token"],
|
|
301
|
-
)
|
|
440
|
+
Token Movement:
|
|
441
|
+
- Service NFT: Owner → Staking Contract (via transferFrom)
|
|
442
|
+
- OLAS tokens: None! Already in Token Utility
|
|
302
443
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
444
|
+
Why No OLAS Transfer?
|
|
445
|
+
The staking contract calls ServiceRegistryTokenUtility.getOperatorBalance()
|
|
446
|
+
to verify the deposited amounts. It doesn't need a new transfer.
|
|
306
447
|
|
|
307
|
-
|
|
308
|
-
|
|
448
|
+
Returns:
|
|
449
|
+
True if stake succeeded and ServiceStaked event was found.
|
|
309
450
|
|
|
310
|
-
|
|
311
|
-
"
|
|
451
|
+
"""
|
|
452
|
+
logger.debug("[STAKE] Preparing stake transaction...")
|
|
312
453
|
stake_tx = staking_contract.prepare_stake_tx(
|
|
313
454
|
from_address=self.wallet.master_account.address,
|
|
314
455
|
service_id=self.service.service_id,
|
|
315
456
|
)
|
|
457
|
+
logger.debug(f"[STAKE] TX prepared: to={stake_tx.get('to')}")
|
|
316
458
|
|
|
317
459
|
success, receipt = self.wallet.sign_and_send_transaction(
|
|
318
460
|
transaction=stake_tx,
|
|
@@ -323,33 +465,38 @@ class StakingManagerMixin:
|
|
|
323
465
|
|
|
324
466
|
if not success:
|
|
325
467
|
if receipt and "status" in receipt and receipt["status"] == 0:
|
|
326
|
-
logger.error(f"
|
|
327
|
-
logger.error("
|
|
468
|
+
logger.error(f"[STAKE] TX reverted. Receipt: {receipt}")
|
|
469
|
+
logger.error("[STAKE] Stake transaction failed")
|
|
328
470
|
return False
|
|
329
471
|
|
|
330
|
-
|
|
472
|
+
tx_hash = get_tx_hash(receipt)
|
|
473
|
+
logger.info(f"[STAKE] TX sent: {tx_hash}")
|
|
331
474
|
|
|
332
475
|
events = staking_contract.extract_events(receipt)
|
|
333
476
|
event_names = [event["name"] for event in events]
|
|
477
|
+
logger.debug(f"[STAKE] Events: {event_names}")
|
|
334
478
|
|
|
335
479
|
if "ServiceStaked" not in event_names:
|
|
336
|
-
logger.error("
|
|
480
|
+
logger.error("[STAKE] ServiceStaked event not found")
|
|
337
481
|
return False
|
|
482
|
+
logger.debug("[STAKE] ServiceStaked event confirmed")
|
|
338
483
|
|
|
339
484
|
# Verify state
|
|
340
485
|
staking_state = staking_contract.get_staking_state(self.service.service_id)
|
|
486
|
+
logger.debug(f"[STAKE] Final staking state: {staking_state.name}")
|
|
487
|
+
|
|
341
488
|
if staking_state != StakingState.STAKED:
|
|
342
|
-
logger.error("Service
|
|
489
|
+
logger.error(f"[STAKE] FAIL: Service not staked (state={staking_state.name})")
|
|
343
490
|
return False
|
|
344
491
|
|
|
345
492
|
# Update local state
|
|
346
493
|
self.service.staking_contract_address = EthereumAddress(staking_contract.address)
|
|
347
494
|
self._update_and_save_service_state()
|
|
348
495
|
|
|
349
|
-
logger.info("Service
|
|
496
|
+
logger.info(f"[STAKE] Service {self.service.service_id} is now STAKED")
|
|
350
497
|
return True
|
|
351
498
|
|
|
352
|
-
def unstake(self, staking_contract) -> bool:
|
|
499
|
+
def unstake(self, staking_contract) -> bool: # noqa: C901
|
|
353
500
|
"""Unstake the service from the staking contract."""
|
|
354
501
|
if not self.service:
|
|
355
502
|
logger.error("No active service")
|
|
@@ -410,13 +557,13 @@ class StakingManagerMixin:
|
|
|
410
557
|
chain_name=self.chain_name,
|
|
411
558
|
tags=["olas_unstake_service"],
|
|
412
559
|
)
|
|
413
|
-
|
|
414
560
|
if not success:
|
|
415
561
|
logger.error(f"Failed to unstake service {self.service.service_id}: Transaction failed")
|
|
416
562
|
return False
|
|
417
563
|
|
|
564
|
+
tx_hash = get_tx_hash(receipt)
|
|
418
565
|
logger.info(
|
|
419
|
-
f"Unstake transaction sent: {
|
|
566
|
+
f"Unstake transaction sent: {tx_hash if receipt else 'No Receipt'}"
|
|
420
567
|
)
|
|
421
568
|
|
|
422
569
|
events = staking_contract.extract_events(receipt)
|
|
@@ -118,6 +118,8 @@ def test_service_manager_complex_registration(mock_erc20_cls, mock_wallet):
|
|
|
118
118
|
manager.wallet.transfer_service.approve_erc20.return_value = True
|
|
119
119
|
manager.registry.extract_events.return_value = [{"name": "RegisterInstance"}]
|
|
120
120
|
mock_erc20_cls.return_value.balance_of_wei.return_value = 1000
|
|
121
|
+
# Fix: Mock allowance to return an int, not MagicMock
|
|
122
|
+
manager.wallet.transfer_service.get_erc20_allowance.return_value = 0
|
|
121
123
|
assert manager.register_agent(VALID_ADDR, 100) is True
|
|
122
124
|
|
|
123
125
|
# deploy successes
|
|
@@ -160,10 +162,12 @@ def test_service_manager_config_edges(mock_wallet):
|
|
|
160
162
|
"""Test ServiceManager configuration and initialization edge cases."""
|
|
161
163
|
with patch("iwa.plugins.olas.service_manager.Config") as mock_cfg_cls:
|
|
162
164
|
mock_cfg = mock_cfg_cls.return_value
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
olas_mock = MagicMock()
|
|
166
|
+
olas_mock.get_service.return_value = Service(
|
|
165
167
|
service_name="t", chain_name="gnosis", service_id=1, agent_ids=[1]
|
|
166
168
|
)
|
|
169
|
+
# Ensure plugins.get("olas") returns our mock
|
|
170
|
+
mock_cfg.plugins = {"olas": olas_mock}
|
|
167
171
|
with patch("iwa.plugins.olas.service_manager.ChainInterfaces"):
|
|
168
172
|
# hits 56
|
|
169
173
|
with patch(
|
|
@@ -48,7 +48,12 @@ class TestOlasPlugin:
|
|
|
48
48
|
|
|
49
49
|
mock_wallet_class.assert_called_once()
|
|
50
50
|
mock_sm_class.assert_called_once_with(mock_wallet)
|
|
51
|
-
mock_manager.create.assert_called_once_with(
|
|
51
|
+
mock_manager.create.assert_called_once_with(
|
|
52
|
+
chain_name="gnosis",
|
|
53
|
+
service_owner_address_or_tag="0x1234",
|
|
54
|
+
token_address_or_tag="OLAS",
|
|
55
|
+
bond_amount_wei=100,
|
|
56
|
+
)
|
|
52
57
|
|
|
53
58
|
@patch("iwa.plugins.olas.plugin.Wallet")
|
|
54
59
|
@patch("iwa.plugins.olas.plugin.ServiceManager")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Integration tests for OlasPlugin."""
|
|
2
2
|
|
|
3
|
-
from unittest.mock import
|
|
3
|
+
from unittest.mock import patch
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
import typer
|
|
@@ -103,15 +103,18 @@ def test_import_services_cli_full(plugin, runner):
|
|
|
103
103
|
def test_get_safe_signers_edge_cases(plugin):
|
|
104
104
|
"""Test _get_safe_signers with various failure scenarios."""
|
|
105
105
|
# 1. No RPC configured
|
|
106
|
-
with patch("iwa.core.
|
|
107
|
-
|
|
106
|
+
with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
|
|
107
|
+
mock_ci = mock_ci_cls.return_value
|
|
108
|
+
mock_ci.get.return_value.chain.rpcs = []
|
|
108
109
|
signers, exists = plugin._get_safe_signers("0x1", "gnosis")
|
|
109
110
|
assert signers is None
|
|
110
111
|
assert exists is None
|
|
111
112
|
|
|
112
113
|
# 2. Safe doesn't exist (raises exception)
|
|
113
|
-
with patch("iwa.core.
|
|
114
|
-
|
|
114
|
+
with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
|
|
115
|
+
mock_ci = mock_ci_cls.return_value
|
|
116
|
+
mock_ci.get.return_value.chain.rpcs = ["http://rpc"]
|
|
117
|
+
mock_ci.get.return_value.chain.rpc = "http://rpc"
|
|
115
118
|
with patch("safe_eth.eth.EthereumClient"), patch("safe_eth.safe.Safe") as mock_safe_cls:
|
|
116
119
|
mock_safe = mock_safe_cls.return_value
|
|
117
120
|
mock_safe.retrieve_owners.side_effect = Exception("Generic error")
|
|
@@ -121,8 +124,10 @@ def test_get_safe_signers_edge_cases(plugin):
|
|
|
121
124
|
assert exists is False
|
|
122
125
|
|
|
123
126
|
# 3. Success path
|
|
124
|
-
with patch("iwa.core.
|
|
125
|
-
|
|
127
|
+
with patch("iwa.core.chain.ChainInterfaces") as mock_ci_cls:
|
|
128
|
+
mock_ci = mock_ci_cls.return_value
|
|
129
|
+
mock_ci.get.return_value.chain.rpcs = ["http://rpc"]
|
|
130
|
+
mock_ci.get.return_value.chain.rpc = "http://rpc"
|
|
126
131
|
with patch("safe_eth.eth.EthereumClient"), patch("safe_eth.safe.Safe") as mock_safe_cls:
|
|
127
132
|
mock_safe = mock_safe_cls.return_value
|
|
128
133
|
mock_safe.retrieve_owners.return_value = ["0xAgent"]
|
|
@@ -95,10 +95,7 @@ def test_sm_stake_fail(sm):
|
|
|
95
95
|
sm.transfer_service.approve_erc20.return_value = True
|
|
96
96
|
sm.wallet.sign_and_send_transaction.return_value = (False, None)
|
|
97
97
|
|
|
98
|
-
with (
|
|
99
|
-
patch("iwa.plugins.olas.contracts.staking.StakingContract") as mock_stk_cls,
|
|
100
|
-
patch("iwa.plugins.olas.service_manager.staking.ERC20Contract"),
|
|
101
|
-
):
|
|
98
|
+
with patch("iwa.plugins.olas.contracts.staking.StakingContract") as mock_stk_cls:
|
|
102
99
|
mock_stk = mock_stk_cls.return_value
|
|
103
100
|
mock_stk.get_service_info.return_value = {"staking_state": 1}
|
|
104
101
|
mock_stk.staking_token_address = addr
|