iwa 0.0.10__py3-none-any.whl → 0.0.12__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.
@@ -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,7 +50,6 @@ 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
11
54
  from iwa.core.utils import get_tx_hash
12
55
  from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
@@ -14,7 +57,21 @@ from iwa.plugins.olas.models import StakingStatus
14
57
 
15
58
 
16
59
  class StakingManagerMixin:
17
- """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
+ """
18
75
 
19
76
  def get_staking_status(self) -> Optional[StakingStatus]:
20
77
  """Get comprehensive staking status for the active service.
@@ -154,20 +211,28 @@ class StakingManagerMixin:
154
211
  def stake(self, staking_contract) -> bool:
155
212
  """Stake the service in a staking contract.
156
213
 
157
- Token Flow:
158
- The total OLAS required is split 50/50 between deposit and bond:
159
- - minStakingDeposit: Transferred to staking contract during this call
160
- - agentBond: Already in Token Utility from service registration
161
-
162
- Example for Hobbyist 1 (100 OLAS total):
163
- - minStakingDeposit: 50 OLAS (from master account -> staking contract)
164
- - agentBond: 50 OLAS (already in Token Utility)
165
-
166
- Requirements:
167
- - Service must be in DEPLOYED state
168
- - Service must be created with OLAS token (not native currency)
169
- - Master account must have >= minStakingDeposit OLAS tokens
170
- - 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
171
236
 
172
237
  Args:
173
238
  staking_contract: StakingContract instance to stake in.
@@ -194,12 +259,12 @@ class StakingManagerMixin:
194
259
  f"[STAKE] Min deposit required: {min_deposit} wei ({min_deposit / 1e18:.2f} OLAS)"
195
260
  )
196
261
 
197
- # 2. Approve Tokens
198
- logger.info("[STAKE] Step 2: Approving tokens...")
199
- if not self._approve_staking_tokens(staking_contract, min_deposit):
200
- logger.error("[STAKE] Step 2 FAILED: Token approval failed")
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")
201
266
  return False
202
- logger.info("[STAKE] Step 2 OK: Tokens approved")
267
+ logger.info("[STAKE] Step 2 OK: Service NFT approved")
203
268
 
204
269
  # 3. Execute Stake Transaction
205
270
  logger.info("[STAKE] Step 3: Executing stake transaction...")
@@ -214,7 +279,27 @@ class StakingManagerMixin:
214
279
  return result
215
280
 
216
281
  def _check_stake_requirements(self, staking_contract) -> Optional[dict]:
217
- """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
+ """
218
303
  from iwa.plugins.olas.contracts.service import ServiceState
219
304
 
220
305
  logger.debug("[STAKE] Fetching contract requirements...")
@@ -291,25 +376,36 @@ class StakingManagerMixin:
291
376
  return None
292
377
  logger.debug("[STAKE] OK: Slots available")
293
378
 
294
- # Check OLAS balance
295
- logger.debug("[STAKE] Checking master OLAS balance...")
296
- erc20_contract = ERC20Contract(staking_token)
297
- master_balance = erc20_contract.balance_of_wei(self.wallet.master_account.address)
298
- logger.info(
299
- f"[STAKE] Master OLAS balance: {master_balance} wei "
300
- f"({master_balance / 1e18:.2f} OLAS, need {min_deposit / 1e18:.2f} OLAS)"
301
- )
302
-
303
- if master_balance < min_deposit:
304
- logger.error(f"[STAKE] FAIL: Insufficient balance ({master_balance} < {min_deposit})")
305
- return None
306
- logger.debug("[STAKE] OK: Sufficient balance")
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")
307
383
 
308
384
  return {"min_deposit": min_deposit, "staking_token": staking_token}
309
385
 
310
- def _approve_staking_tokens(self, staking_contract, min_deposit: int) -> bool:
311
- """Approve both the service NFT and OLAS tokens for staking."""
312
- # 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
313
409
  logger.debug("[STAKE] Approving service NFT for staking contract...")
314
410
  approve_tx = self.registry.prepare_approve_tx(
315
411
  from_address=self.wallet.master_account.address,
@@ -330,36 +426,29 @@ class StakingManagerMixin:
330
426
 
331
427
  tx_hash = get_tx_hash(receipt)
332
428
  logger.info(f"[STAKE] Service NFT approved: {tx_hash}")
429
+ return True
333
430
 
334
- # Approve OLAS tokens
335
- logger.debug(f"[STAKE] Approving OLAS tokens ({min_deposit} wei)...")
336
- reqs = staking_contract.get_requirements()
337
- staking_token = Web3.to_checksum_address(reqs["staking_token"])
338
- erc20_contract = ERC20Contract(staking_token)
431
+ def _execute_stake_transaction(self, staking_contract) -> bool:
432
+ """Execute the actual stake transaction on the staking contract.
339
433
 
340
- olas_approve_tx = erc20_contract.prepare_approve_tx(
341
- from_address=self.wallet.master_account.address,
342
- spender=staking_contract.address,
343
- amount_wei=min_deposit,
344
- )
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
345
439
 
346
- success, receipt = self.wallet.sign_and_send_transaction(
347
- transaction=olas_approve_tx,
348
- signer_address_or_tag=self.wallet.master_account.address,
349
- chain_name=self.chain_name,
350
- tags=["olas_approve_olas_token"],
351
- )
440
+ Token Movement:
441
+ - Service NFT: Owner → Staking Contract (via transferFrom)
442
+ - OLAS tokens: None! Already in Token Utility
352
443
 
353
- if not success:
354
- logger.error("[STAKE] FAIL: OLAS token approval failed")
355
- 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.
356
447
 
357
- tx_hash = get_tx_hash(receipt)
358
- logger.info(f"[STAKE] OLAS tokens approved: {tx_hash}")
359
- return True
448
+ Returns:
449
+ True if stake succeeded and ServiceStaked event was found.
360
450
 
361
- def _execute_stake_transaction(self, staking_contract) -> bool:
362
- """Send the stake transaction and verify the result."""
451
+ """
363
452
  logger.debug("[STAKE] Preparing stake transaction...")
364
453
  stake_tx = staking_contract.prepare_stake_tx(
365
454
  from_address=self.wallet.master_account.address,
@@ -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(
@@ -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
@@ -100,11 +100,6 @@ def mock_chain_interfaces():
100
100
  yield mock
101
101
 
102
102
 
103
- @pytest.fixture
104
- def mock_erc20_contract():
105
- """Mock ERC20 contract fixture."""
106
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock:
107
- yield mock
108
103
 
109
104
 
110
105
  @pytest.fixture
@@ -114,7 +109,7 @@ def service_manager(
114
109
  mock_registry,
115
110
  mock_manager_contract,
116
111
  mock_chain_interfaces,
117
- mock_erc20_contract,
112
+
118
113
  mock_olas_config,
119
114
  mock_service,
120
115
  ):
@@ -174,12 +169,11 @@ def test_create_no_event(service_manager, mock_wallet):
174
169
  mock_wallet.sign_and_send_transaction.return_value = (True, {})
175
170
  service_manager.registry.extract_events.return_value = []
176
171
 
177
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract"): # Mock ERC20
178
- res = service_manager.create(
179
- token_address_or_tag="0x1111111111111111111111111111111111111111"
180
- )
181
- # create() finds no ID, logs error, returns None for service_id.
182
- assert res is None
172
+ res = service_manager.create(
173
+ token_address_or_tag="0x1111111111111111111111111111111111111111"
174
+ )
175
+ # create() finds no ID, logs error, returns None for service_id.
176
+ assert res is None
183
177
 
184
178
 
185
179
  def test_activate_registration_success(service_manager, mock_wallet):
@@ -193,7 +187,7 @@ def test_activate_registration_success(service_manager, mock_wallet):
193
187
 
194
188
  # Mock balance/allowance for the new check
195
189
  mock_wallet.balance_service = MagicMock()
196
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
190
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
197
191
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
198
192
 
199
193
  assert service_manager.activate_registration() is True
@@ -287,18 +281,15 @@ def test_stake_success(service_manager, mock_wallet):
287
281
  "security_deposit": 50000000000000000000,
288
282
  }
289
283
 
290
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock_erc20:
291
- mock_erc20.return_value.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
292
-
293
- mock_wallet.sign_and_send_transaction.return_value = (True, {})
294
- staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
295
- staking_contract.get_staking_state.return_value = StakingState.STAKED
284
+ mock_wallet.sign_and_send_transaction.return_value = (True, {})
285
+ staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
286
+ staking_contract.get_staking_state.return_value = StakingState.STAKED
296
287
 
297
- # We need to make sure prepare_approve_tx is mocked ON THE REGISTRY INSTANCE
298
- service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
288
+ # We need to make sure prepare_approve_tx is mocked ON THE REGISTRY INSTANCE
289
+ service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
299
290
 
300
- assert service_manager.stake(staking_contract) is True
301
- assert service_manager.service.staking_contract_address == TEST_STAKING_ADDR
291
+ assert service_manager.stake(staking_contract) is True
292
+ assert service_manager.service.staking_contract_address == TEST_STAKING_ADDR
302
293
 
303
294
 
304
295
  def test_unstake_success(service_manager, mock_wallet):
@@ -370,60 +361,15 @@ def test_register_agent_fund_fails(service_manager, mock_wallet):
370
361
 
371
362
  def test_spin_up_from_pre_registration_success(service_manager, mock_wallet):
372
363
  """Test full spin_up path from PRE_REGISTRATION to DEPLOYED."""
373
- # Mock state transitions - need to match actual calls in spin_up
374
- # The state after activate_registration should be ACTIVE_REGISTRATION
375
- state_sequence = [
376
- {
377
- "state": ServiceState.PRE_REGISTRATION,
378
- "security_deposit": 50000000000000000000,
379
- }, # spin_up initial
380
- {
381
- "state": ServiceState.PRE_REGISTRATION,
382
- "security_deposit": 50000000000000000000,
383
- }, # activate_registration check
384
- {
385
- "state": ServiceState.PRE_REGISTRATION,
386
- "security_deposit": 50000000000000000000,
387
- }, # activate_registration internal (get security deposit)
388
- {
389
- "state": ServiceState.ACTIVE_REGISTRATION,
390
- "security_deposit": 50000000000000000000,
391
- }, # spin_up verify after activate
392
- {
393
- "state": ServiceState.ACTIVE_REGISTRATION,
394
- "security_deposit": 50000000000000000000,
395
- }, # register_agent check
396
- {
397
- "state": ServiceState.ACTIVE_REGISTRATION,
398
- "security_deposit": 50000000000000000000,
399
- }, # register_agent internal
400
- {
401
- "state": ServiceState.FINISHED_REGISTRATION,
402
- "security_deposit": 50000000000000000000,
403
- }, # spin_up verify after register
404
- {
405
- "state": ServiceState.FINISHED_REGISTRATION,
406
- "security_deposit": 50000000000000000000,
407
- }, # deploy check
408
- {
409
- "state": ServiceState.DEPLOYED,
410
- "security_deposit": 50000000000000000000,
411
- }, # spin_up verify after deploy
412
- {
413
- "state": ServiceState.DEPLOYED,
414
- "security_deposit": 50000000000000000000,
415
- }, # final verification
364
+ # Use a stateful mock that progresses based on extract_events calls
365
+ states = [
366
+ ServiceState.PRE_REGISTRATION, # Before any TX
367
+ ServiceState.ACTIVE_REGISTRATION, # After 1st TX (activate)
368
+ ServiceState.FINISHED_REGISTRATION, # After 2nd TX (register)
369
+ ServiceState.DEPLOYED, # After 3rd TX (deploy)
416
370
  ]
417
- service_manager.registry.get_service.side_effect = state_sequence
418
-
419
- mock_wallet.send.return_value = "0xMockTxHash" # wallet.send returns tx_hash
420
- mock_wallet.sign_and_send_transaction.return_value = (True, {})
421
-
422
- # Mock balance/allowance for activate_registration internal call
423
- mock_wallet.balance_service = MagicMock()
424
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
425
- mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
426
- service_manager.registry.extract_events.side_effect = [
371
+ tx_count = [0] # Track completed transactions
372
+ events_side_effects = [
427
373
  [{"name": "ActivateRegistration"}],
428
374
  [{"name": "RegisterInstance"}],
429
375
  [
@@ -431,6 +377,32 @@ def test_spin_up_from_pre_registration_success(service_manager, mock_wallet):
431
377
  {"name": "CreateMultisigWithAgents", "args": {"multisig": TEST_MULTISIG_ADDR}},
432
378
  ],
433
379
  ]
380
+ event_idx = [0]
381
+
382
+ def dynamic_state(*args, **kwargs):
383
+ """Return state based on completed transactions."""
384
+ state = states[min(tx_count[0], len(states) - 1)]
385
+ return {"state": state, "security_deposit": 50000000000000000000}
386
+
387
+ def extract_and_progress(*args, **kwargs):
388
+ """Return events and advance transaction counter."""
389
+ if event_idx[0] < len(events_side_effects):
390
+ events = events_side_effects[event_idx[0]]
391
+ event_idx[0] += 1
392
+ tx_count[0] += 1
393
+ return events
394
+ return []
395
+
396
+ service_manager.registry.get_service.side_effect = dynamic_state
397
+ service_manager.registry.extract_events.side_effect = extract_and_progress
398
+
399
+ mock_wallet.send.return_value = "0xMockTxHash"
400
+ mock_wallet.sign_and_send_transaction.return_value = (True, {})
401
+
402
+ # Mock balance/allowance for activate_registration internal call
403
+ mock_wallet.balance_service = MagicMock()
404
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
405
+ mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
434
406
 
435
407
  assert service_manager.spin_up() is True
436
408
 
@@ -557,14 +529,12 @@ def test_spin_up_with_staking(service_manager, mock_wallet):
557
529
  "required_agent_bond": 50000000000000000000, # 50 OLAS
558
530
  }
559
531
 
560
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") as mock_erc20:
561
- mock_erc20.return_value.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
562
- mock_wallet.sign_and_send_transaction.return_value = (True, {})
563
- staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
564
- staking_contract.get_staking_state.return_value = StakingState.STAKED
565
- service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
532
+ mock_wallet.sign_and_send_transaction.return_value = (True, {})
533
+ staking_contract.extract_events.return_value = [{"name": "ServiceStaked"}]
534
+ staking_contract.get_staking_state.return_value = StakingState.STAKED
535
+ service_manager.registry.prepare_approve_tx.return_value = {"to": "0xApprove"}
566
536
 
567
- assert service_manager.spin_up(staking_contract=staking_contract) is True
537
+ assert service_manager.spin_up(staking_contract=staking_contract) is True
568
538
 
569
539
 
570
540
  def test_spin_up_activate_fails(service_manager, mock_wallet):
@@ -587,7 +557,7 @@ def test_spin_up_activate_fails(service_manager, mock_wallet):
587
557
 
588
558
  # Mock balance/allowance for activate_registration behavior
589
559
  mock_wallet.balance_service = MagicMock()
590
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
560
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
591
561
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
592
562
 
593
563
  mock_wallet.sign_and_send_transaction.return_value = (False, {})
@@ -931,7 +901,7 @@ def test_activate_registration_token_service_sends_security_deposit_as_value(
931
901
 
932
902
  # Mock balance check to pass
933
903
  mock_wallet.balance_service = MagicMock()
934
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18 # Plenty of balance
904
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18 # Plenty of balance
935
905
 
936
906
  # Mock allowance to pass check (return an int)
937
907
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20 # Plenty of allowance
@@ -964,7 +934,7 @@ def test_activate_registration_native_service_sends_security_deposit_as_value(
964
934
 
965
935
  # Mock balance/allowance
966
936
  mock_wallet.balance_service = MagicMock()
967
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
937
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
968
938
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
969
939
 
970
940
  service_manager.activate_registration()
@@ -996,7 +966,7 @@ def test_activate_registration_uses_master_account_as_from_address(service_manag
996
966
  # Mock balance/allowance
997
967
  mock_wallet.balance_service = MagicMock()
998
968
  mock_wallet.transfer_service = MagicMock()
999
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
969
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
1000
970
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
1001
971
 
1002
972
  service_manager.activate_registration()
@@ -1028,7 +998,7 @@ def test_activate_registration_uses_master_account_as_signer(service_manager, mo
1028
998
  # Mock balance/allowance
1029
999
  mock_wallet.balance_service = MagicMock()
1030
1000
  mock_wallet.transfer_service = MagicMock()
1031
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
1001
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
1032
1002
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 10**20
1033
1003
 
1034
1004
  service_manager.activate_registration()
@@ -1055,7 +1025,7 @@ def test_activate_registration_token_service_approves_token_utility(service_mana
1055
1025
 
1056
1026
  # Mock low allowance to trigger approval
1057
1027
  mock_wallet.balance_service = MagicMock()
1058
- mock_wallet.balance_service.get_erc20_balance_wei.return_value = 10**18
1028
+ mock_wallet.balance_service.get_erc20_balance_wei.return_value = 100 * 10**18
1059
1029
  mock_wallet.transfer_service.get_erc20_allowance.return_value = 0 # Low allowance
1060
1030
  mock_wallet.transfer_service.approve_erc20.return_value = True
1061
1031
 
@@ -121,8 +121,7 @@ def test_service_manager_lifecycle_failures(mock_wallet):
121
121
  "num_agent_instances": 1,
122
122
  "required_agent_bond": 50000000000000000000,
123
123
  }
124
- with patch("iwa.plugins.olas.service_manager.staking.ERC20Contract"):
125
- assert manager.stake(mock_staking) is False
124
+ assert manager.stake(mock_staking) is False
126
125
 
127
126
  # unstake failures
128
127
  # Service not staked
@@ -45,9 +45,8 @@ def mock_config():
45
45
  @patch("iwa.plugins.olas.service_manager.base.Config")
46
46
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
47
47
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
48
- @patch("iwa.plugins.olas.service_manager.staking.ERC20Contract")
49
48
  def test_create_service_success(
50
- mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
49
+ mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
51
50
  ):
52
51
  """Test successful service creation."""
53
52
  # Setup Config with new OlasConfig structure
@@ -85,9 +84,8 @@ def test_create_service_success(
85
84
  @patch("iwa.plugins.olas.service_manager.base.Config")
86
85
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
87
86
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
88
- @patch("iwa.plugins.olas.service_manager.staking.ERC20Contract")
89
87
  def test_create_service_failures(
90
- mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
88
+ mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
91
89
  ):
92
90
  """Test service creation failure modes."""
93
91
  mock_config_inst = mock_config_cls.return_value
@@ -141,9 +139,8 @@ def test_create_service_failures(
141
139
  @patch("iwa.plugins.olas.service_manager.base.Config")
142
140
  @patch("iwa.plugins.olas.service_manager.base.ServiceRegistryContract")
143
141
  @patch("iwa.plugins.olas.service_manager.base.ServiceManagerContract")
144
- @patch("iwa.plugins.olas.service_manager.staking.ERC20Contract")
145
142
  def test_create_service_with_approval(
146
- mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
143
+ mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet
147
144
  ):
148
145
  """Test service creation with token approval."""
149
146
  mock_config_inst = mock_config_cls.return_value
@@ -404,8 +401,7 @@ def test_terminate(mock_sm_contract, mock_registry_contract, mock_config_cls, mo
404
401
  @patch(
405
402
  "iwa.plugins.olas.service_manager.base.ServiceManagerContract"
406
403
  ) # MUST mock specifically here
407
- @patch("iwa.plugins.olas.service_manager.staking.ERC20Contract") # For checking balance
408
- def test_stake(mock_erc20, mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
404
+ def test_stake(mock_sm_contract, mock_registry_contract, mock_config_cls, mock_wallet):
409
405
  """Test service staking."""
410
406
  # Setup mock service
411
407
  mock_service = MagicMock()
@@ -445,9 +441,6 @@ def test_stake(mock_erc20, mock_sm_contract, mock_registry_contract, mock_config
445
441
  "required_agent_bond": 50000000000000000000,
446
442
  }
447
443
 
448
- # Mock ERC20 balance check (100 OLAS = 100e18 wei, enough for 50 min deposit)
449
- mock_erc20_inst = mock_erc20.return_value
450
- mock_erc20_inst.balance_of_wei.return_value = 100000000000000000000 # 100 OLAS
451
444
 
452
445
  success = manager.stake(mock_staking)
453
446
  assert success is True
@@ -471,10 +464,7 @@ def test_stake(mock_erc20, mock_sm_contract, mock_registry_contract, mock_config
471
464
  assert manager.stake(mock_staking) is False
472
465
  mock_staking.get_service_ids.return_value = []
473
466
 
474
- # 3. Not enough funds
475
- mock_erc20_inst.balance_of_wei.return_value = 50
476
- assert manager.stake(mock_staking) is False
477
- mock_erc20_inst.balance_of_wei.return_value = 200
467
+
478
468
 
479
469
  # 4. Approve fail
480
470
  mock_wallet.sign_and_send_transaction.return_value = (False, {})