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
@@ -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").hex() if success else None
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
- Token Flow:
157
- The total OLAS required is split 50/50 between deposit and bond:
158
- - minStakingDeposit: Transferred to staking contract during this call
159
- - agentBond: Already in Token Utility from service registration
160
-
161
- Example for Hobbyist 1 (100 OLAS total):
162
- - minStakingDeposit: 50 OLAS (from master account -> staking contract)
163
- - agentBond: 50 OLAS (already in Token Utility)
164
-
165
- Requirements:
166
- - Service must be in DEPLOYED state
167
- - Service must be created with OLAS token (not native currency)
168
- - Master account must have >= minStakingDeposit OLAS tokens
169
- - Staking contract must have available slots
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 Tokens
186
- if not self._approve_staking_tokens(staking_contract, min_deposit):
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
- return self._execute_stake_transaction(staking_contract)
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
- # Check centralized staking requirements
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(f"Checking stake requirements for service {self.service.service_id}")
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 not deployed, cannot stake")
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"Token mismatch: service was created with {service_token or 'native'}, "
219
- f"but staking contract requires {staking_token_lower}"
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 for service")
347
+ logger.error("[STAKE] FAIL: No agent IDs found")
228
348
  return None
229
349
 
230
- agent_id = agent_ids[0]
231
- agent_params = self.registry.get_agent_params(self.service.service_id, agent_id)
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.error(
236
- f"Service agent bond is too low ({current_bond} < {required_bond}). "
237
- "Service must be created with the correct bond amount to be stakeable."
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
- return None
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("Staking contract is full, no free slots available")
375
+ logger.error("[STAKE] FAIL: No free slots")
248
376
  return None
377
+ logger.debug("[STAKE] OK: Slots available")
249
378
 
250
- # Check OLAS balance
251
- erc20_contract = ERC20Contract(staking_token)
252
- master_balance = erc20_contract.balance_of_wei(self.wallet.master_account.address)
253
- if master_balance < min_deposit:
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, min_deposit: int) -> bool:
262
- """Approve both the service NFT and OLAS tokens for staking."""
263
- # Approve service NFT
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("Failed to approve staking contract [Service Registry]")
424
+ logger.error("[STAKE] FAIL: Service NFT approval failed")
279
425
  return False
280
426
 
281
- logger.info("Service token approved for staking contract")
427
+ tx_hash = get_tx_hash(receipt)
428
+ logger.info(f"[STAKE] Service NFT approved: {tx_hash}")
429
+ return True
282
430
 
283
- # Approve OLAS tokens
284
- # We need to get the token address from the contract requirements again or pass it
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
- olas_approve_tx = erc20_contract.prepare_approve_tx(
291
- from_address=self.wallet.master_account.address,
292
- spender=staking_contract.address,
293
- amount_wei=min_deposit,
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
- success, receipt = self.wallet.sign_and_send_transaction(
297
- transaction=olas_approve_tx,
298
- signer_address_or_tag=self.wallet.master_account.address,
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
- if not success:
304
- logger.error("Failed to approve OLAS tokens for staking contract")
305
- return False
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
- logger.info("OLAS tokens approved for staking contract")
308
- return True
448
+ Returns:
449
+ True if stake succeeded and ServiceStaked event was found.
309
450
 
310
- def _execute_stake_transaction(self, staking_contract) -> bool:
311
- """Send the stake transaction and verify the result."""
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"Stake transaction reverted. Receipt: {receipt}")
327
- logger.error("Failed to stake service")
468
+ logger.error(f"[STAKE] TX reverted. Receipt: {receipt}")
469
+ logger.error("[STAKE] Stake transaction failed")
328
470
  return False
329
471
 
330
- logger.info("Service stake transaction sent successfully")
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("Stake service event not found")
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 is not staked after transaction")
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 staked successfully")
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: {receipt.get('transactionHash', '').hex() if receipt else 'No Receipt'}"
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
- mock_cfg.plugins = {"olas": MagicMock()}
164
- mock_cfg.plugins["olas"].get_service.return_value = Service(
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("gnosis", "0x1234", "OLAS", 100)
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 MagicMock, patch
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.secrets.secrets") as mock_settings:
107
- mock_settings.gnosis_rpc = None
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.secrets.secrets") as mock_settings:
114
- mock_settings.gnosis_rpc = MagicMock()
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.secrets.secrets") as mock_settings:
125
- mock_settings.gnosis_rpc = MagicMock()
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