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.
- iwa/core/chain/interface.py +21 -7
- iwa/core/cli.py +8 -0
- iwa/core/services/transaction.py +196 -3
- iwa/core/utils.py +38 -0
- iwa/core/wallet.py +2 -1
- iwa/plugins/olas/contracts/abis/service_registry_token_utility.json +926 -0
- iwa/plugins/olas/contracts/service.py +50 -0
- iwa/plugins/olas/service_manager/lifecycle.py +275 -39
- iwa/plugins/olas/service_manager/staking.py +152 -63
- iwa/plugins/olas/tests/test_olas_contracts.py +6 -2
- 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/web/routers/olas/services.py +52 -19
- iwa/web/routers/olas/staking.py +8 -2
- {iwa-0.0.10.dist-info → iwa-0.0.12.dist-info}/METADATA +1 -1
- {iwa-0.0.10.dist-info → iwa-0.0.12.dist-info}/RECORD +23 -22
- tests/test_chain.py +12 -7
- {iwa-0.0.10.dist-info → iwa-0.0.12.dist-info}/WHEEL +0 -0
- {iwa-0.0.10.dist-info → iwa-0.0.12.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.10.dist-info → iwa-0.0.12.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.10.dist-info → iwa-0.0.12.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
198
|
-
logger.info("[STAKE] Step 2: Approving
|
|
199
|
-
if not self._approve_staking_tokens(staking_contract
|
|
200
|
-
logger.error("[STAKE] Step 2 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:
|
|
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
|
-
#
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
311
|
-
"""Approve
|
|
312
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
return True
|
|
448
|
+
Returns:
|
|
449
|
+
True if stake succeeded and ServiceStaked event was found.
|
|
360
450
|
|
|
361
|
-
|
|
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
|
-
|
|
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(
|
|
@@ -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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
#
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {})
|