iwa 0.0.18__py3-none-any.whl → 0.0.20__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/chainlist.py +116 -0
- iwa/core/constants.py +1 -0
- iwa/core/contracts/cache.py +131 -0
- iwa/core/contracts/contract.py +7 -0
- iwa/core/rpc_monitor.py +60 -0
- iwa/plugins/olas/constants.py +41 -39
- iwa/plugins/olas/contracts/abis/mech_marketplace_v1.json +828 -0
- iwa/plugins/olas/contracts/activity_checker.py +63 -25
- iwa/plugins/olas/contracts/mech_marketplace_v1.py +68 -0
- iwa/plugins/olas/contracts/staking.py +115 -19
- iwa/plugins/olas/events.py +141 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +1 -1
- iwa/plugins/olas/service_manager/base.py +7 -2
- iwa/plugins/olas/service_manager/lifecycle.py +30 -5
- iwa/plugins/olas/service_manager/mech.py +251 -42
- iwa/plugins/olas/service_manager/staking.py +6 -2
- iwa/plugins/olas/tests/test_olas_integration.py +38 -10
- iwa/plugins/olas/tests/test_service_manager.py +7 -1
- iwa/plugins/olas/tests/test_service_manager_errors.py +22 -11
- iwa/plugins/olas/tests/test_service_manager_flows.py +24 -8
- iwa/plugins/olas/tests/test_service_staking.py +59 -15
- iwa/plugins/olas/tests/test_staking_validation.py +8 -14
- iwa/tools/reset_tenderly.py +2 -2
- iwa/tools/test_chainlist.py +38 -0
- iwa/web/routers/olas/staking.py +9 -4
- {iwa-0.0.18.dist-info → iwa-0.0.20.dist-info}/METADATA +1 -1
- {iwa-0.0.18.dist-info → iwa-0.0.20.dist-info}/RECORD +32 -24
- tests/test_rpc_efficiency.py +103 -0
- {iwa-0.0.18.dist-info → iwa-0.0.20.dist-info}/WHEEL +0 -0
- {iwa-0.0.18.dist-info → iwa-0.0.20.dist-info}/entry_points.txt +0 -0
- {iwa-0.0.18.dist-info → iwa-0.0.20.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.0.18.dist-info → iwa-0.0.20.dist-info}/top_level.txt +0 -0
|
@@ -1,28 +1,57 @@
|
|
|
1
1
|
"""Mech manager mixin.
|
|
2
2
|
|
|
3
|
-
This module handles sending mech requests for OLAS services. There are
|
|
4
|
-
distinct flows for mech requests, and the correct one
|
|
5
|
-
the service's staking contract:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
This module handles sending mech requests for OLAS services. There are THREE
|
|
4
|
+
distinct flows for mech requests, and the correct one is automatically selected
|
|
5
|
+
based on the service's staking contract configuration:
|
|
6
|
+
|
|
7
|
+
Flow Selection Logic:
|
|
8
|
+
1. `get_marketplace_config()` checks if staking contract's activity checker
|
|
9
|
+
has a non-zero `mechMarketplace` address
|
|
10
|
+
2. If yes → marketplace request (v1 or v2 depending on address)
|
|
11
|
+
3. If no → legacy mech request
|
|
12
|
+
|
|
13
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
14
|
+
│ FLOW 1: Legacy Mech (use_marketplace=False) │
|
|
15
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
16
|
+
│ Contract: Legacy Mech (0x77af31De...) │
|
|
17
|
+
│ Used by: NON-MM staking contracts (e.g., "Expert X (Yk OLAS)") │
|
|
18
|
+
│ Counting: agentMech.getRequestsCount(multisig) │
|
|
19
|
+
│ Method: _send_legacy_mech_request() │
|
|
20
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
21
|
+
|
|
22
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ FLOW 2: Marketplace v2 (use_marketplace=True, marketplace=0x735F...) │
|
|
24
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
25
|
+
│ Contract: MechMarketplace v2 (0x735FAAb1c...) │
|
|
26
|
+
│ Used by: Newer MM staking contracts │
|
|
27
|
+
│ Counting: mechMarketplace.mapRequestCounts(multisig) │
|
|
28
|
+
│ Method: _send_marketplace_mech_request() → MechMarketplaceContract │
|
|
29
|
+
│ Signature: request(bytes,uint256,bytes32,address,uint256,bytes) │
|
|
30
|
+
│ Note: Uses payment types (PAYMENT_TYPE_NATIVE) │
|
|
31
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
32
|
+
|
|
33
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
34
|
+
│ FLOW 3: Marketplace v1 (use_marketplace=True, marketplace=0x4554...) │
|
|
35
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
36
|
+
│ Contract: MechMarketplace v1 (0x4554fE75...) [VERSION 1.0.0] │
|
|
37
|
+
│ Used by: Older MM contracts (e.g., "Expert 17 MM", trader_ant) │
|
|
38
|
+
│ Counting: mechMarketplace.mapRequestCounts(multisig) │
|
|
39
|
+
│ Method: _send_v1_marketplace_request() → MechMarketplaceV1Contract │
|
|
40
|
+
│ Signature: request(bytes,address,address,uint256,address,uint256,uint256) │
|
|
41
|
+
│ Note: Requires staking instance + service ID for mech AND requester │
|
|
42
|
+
│ No payment types - simpler but different parameter set │
|
|
43
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
44
|
+
|
|
45
|
+
Important:
|
|
46
|
+
If a service is staked in an MM contract but sends requests to the wrong
|
|
47
|
+
marketplace (or uses legacy flow), those requests will NOT be counted by
|
|
48
|
+
the activity checker, and the service will not receive staking rewards.
|
|
49
|
+
|
|
50
|
+
The dispatch logic:
|
|
51
|
+
1. _send_marketplace_mech_request() checks if marketplace ∈ V1_MARKETPLACES
|
|
52
|
+
2. If v1 → dispatches to _send_v1_marketplace_request()
|
|
53
|
+
3. If v2 → continues with MechMarketplaceContract (v2 ABI)
|
|
54
|
+
|
|
26
55
|
"""
|
|
27
56
|
|
|
28
57
|
from typing import Optional
|
|
@@ -37,6 +66,31 @@ from iwa.plugins.olas.constants import (
|
|
|
37
66
|
)
|
|
38
67
|
from iwa.plugins.olas.contracts.mech import MechContract
|
|
39
68
|
from iwa.plugins.olas.contracts.mech_marketplace import MechMarketplaceContract
|
|
69
|
+
from iwa.plugins.olas.contracts.mech_marketplace_v1 import (
|
|
70
|
+
MechMarketplaceV1Contract,
|
|
71
|
+
V1RequestParams,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Maps marketplace address to (priority_mech_address, priority_mech_service_id, mech_staking_instance)
|
|
75
|
+
# Source: olas-operate-middleware profiles.py and manage.py
|
|
76
|
+
# The 3rd element (staking instance) is only needed for v1 marketplaces
|
|
77
|
+
DEFAULT_PRIORITY_MECH = {
|
|
78
|
+
"0x4554fE75c1f5576c1d7F765B2A036c199Adae329": (
|
|
79
|
+
"0x552cEA7Bc33CbBEb9f1D90c1D11D2C6daefFd053",
|
|
80
|
+
975,
|
|
81
|
+
"0x998dEFafD094817EF329f6dc79c703f1CF18bC90", # Mech staking instance for v1
|
|
82
|
+
),
|
|
83
|
+
"0x735FAAb1c4Ec41128c367AFb5c3baC73509f70bB": (
|
|
84
|
+
"0xC05e7412439bD7e91730a6880E18d5D5873F632C",
|
|
85
|
+
2182,
|
|
86
|
+
None, # v2 doesn't need staking instance
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Marketplace v1 addresses (use different request signature)
|
|
91
|
+
V1_MARKETPLACES = {
|
|
92
|
+
"0x4554fE75c1f5576c1d7F765B2A036c199Adae329", # VERSION 1.0.0
|
|
93
|
+
}
|
|
40
94
|
|
|
41
95
|
|
|
42
96
|
class MechManagerMixin:
|
|
@@ -72,17 +126,26 @@ class MechManagerMixin:
|
|
|
72
126
|
checker = staking.activity_checker
|
|
73
127
|
|
|
74
128
|
if checker.mech_marketplace and checker.mech_marketplace != ZERO_ADDRESS:
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
129
|
+
# Get priority mech from mapping based on marketplace address
|
|
130
|
+
marketplace_addr = Web3.to_checksum_address(checker.mech_marketplace)
|
|
131
|
+
priority_mech_info = DEFAULT_PRIORITY_MECH.get(marketplace_addr)
|
|
132
|
+
|
|
133
|
+
if priority_mech_info:
|
|
134
|
+
priority_mech = priority_mech_info[0] # First element is mech address
|
|
135
|
+
else:
|
|
136
|
+
# Fallback to constants if marketplace not in mapping
|
|
137
|
+
protocol_contracts = OLAS_CONTRACTS.get(self.chain_name, {})
|
|
138
|
+
priority_mech = protocol_contracts.get("OLAS_MECH_MARKETPLACE_PRIORITY")
|
|
139
|
+
logger.warning(
|
|
140
|
+
f"[MECH] Marketplace {marketplace_addr} not in DEFAULT_PRIORITY_MECH, "
|
|
141
|
+
f"using fallback priority_mech: {priority_mech}"
|
|
142
|
+
)
|
|
80
143
|
|
|
81
144
|
logger.info(
|
|
82
145
|
f"[MECH] Service {self.service.service_id} requires marketplace requests "
|
|
83
|
-
f"(marketplace={
|
|
146
|
+
f"(marketplace={marketplace_addr}, priority_mech={priority_mech})"
|
|
84
147
|
)
|
|
85
|
-
return (True,
|
|
148
|
+
return (True, marketplace_addr, priority_mech)
|
|
86
149
|
|
|
87
150
|
return (False, None, None)
|
|
88
151
|
|
|
@@ -134,6 +197,7 @@ class MechManagerMixin:
|
|
|
134
197
|
return None
|
|
135
198
|
|
|
136
199
|
# Auto-detect marketplace requirement if not explicitly specified
|
|
200
|
+
detected_marketplace = None
|
|
137
201
|
if use_marketplace is None:
|
|
138
202
|
use_marketplace, detected_marketplace, detected_priority_mech = (
|
|
139
203
|
self.get_marketplace_config()
|
|
@@ -143,10 +207,12 @@ class MechManagerMixin:
|
|
|
143
207
|
mech_address = mech_address or detected_marketplace
|
|
144
208
|
|
|
145
209
|
if use_marketplace:
|
|
210
|
+
# Use detected marketplace if available, otherwise _send_marketplace_mech_request
|
|
211
|
+
# will fall back to constant
|
|
146
212
|
return self._send_marketplace_mech_request(
|
|
147
213
|
data=data,
|
|
148
214
|
value=value,
|
|
149
|
-
marketplace_address=
|
|
215
|
+
marketplace_address=detected_marketplace,
|
|
150
216
|
priority_mech=priority_mech,
|
|
151
217
|
max_delivery_rate=max_delivery_rate,
|
|
152
218
|
payment_type=payment_type,
|
|
@@ -206,7 +272,12 @@ class MechManagerMixin:
|
|
|
206
272
|
)
|
|
207
273
|
|
|
208
274
|
def _validate_priority_mech(self, marketplace, priority_mech: str) -> bool:
|
|
209
|
-
"""Validate priority mech is registered on marketplace.
|
|
275
|
+
"""Validate priority mech is registered on marketplace.
|
|
276
|
+
|
|
277
|
+
Note: OLD marketplace v1 (0x4554...) doesn't have checkMech function.
|
|
278
|
+
In that case, we skip validation and proceed - v1 doesn't require
|
|
279
|
+
mech registration.
|
|
280
|
+
"""
|
|
210
281
|
try:
|
|
211
282
|
mech_multisig = marketplace.call("checkMech", priority_mech)
|
|
212
283
|
if mech_multisig == ZERO_ADDRESS:
|
|
@@ -214,8 +285,19 @@ class MechManagerMixin:
|
|
|
214
285
|
return False
|
|
215
286
|
logger.debug(f"Priority mech {priority_mech} -> multisig {mech_multisig}")
|
|
216
287
|
except Exception as e:
|
|
217
|
-
|
|
218
|
-
|
|
288
|
+
# Check if this is a revert (v1 doesn't have checkMech) vs a network error
|
|
289
|
+
error_str = str(e).lower()
|
|
290
|
+
if "reverted" in error_str or "execution reverted" in error_str:
|
|
291
|
+
# v1 marketplaces don't have checkMech - skip validation
|
|
292
|
+
logger.warning(
|
|
293
|
+
f"Could not validate priority mech (marketplace may be v1): {e}. "
|
|
294
|
+
"Proceeding without validation."
|
|
295
|
+
)
|
|
296
|
+
return True
|
|
297
|
+
else:
|
|
298
|
+
# Real error (network, timeout, etc.) - fail validation
|
|
299
|
+
logger.error(f"Failed to validate priority mech (network error?): {e}")
|
|
300
|
+
return False
|
|
219
301
|
|
|
220
302
|
# Log mech factory info (optional validation)
|
|
221
303
|
try:
|
|
@@ -234,7 +316,11 @@ class MechManagerMixin:
|
|
|
234
316
|
def _validate_marketplace_params(
|
|
235
317
|
self, marketplace, response_timeout: int, payment_type: bytes
|
|
236
318
|
) -> bool:
|
|
237
|
-
"""Validate marketplace parameters.
|
|
319
|
+
"""Validate marketplace parameters.
|
|
320
|
+
|
|
321
|
+
Note: v1 marketplaces may not have all validation functions.
|
|
322
|
+
We proceed with warnings when validation functions are unavailable.
|
|
323
|
+
"""
|
|
238
324
|
# Validate response_timeout bounds
|
|
239
325
|
try:
|
|
240
326
|
min_timeout = marketplace.call("minResponseTimeout")
|
|
@@ -250,15 +336,31 @@ class MechManagerMixin:
|
|
|
250
336
|
except Exception as e:
|
|
251
337
|
logger.warning(f"Could not validate response_timeout bounds: {e}")
|
|
252
338
|
|
|
253
|
-
# Validate payment type has balance tracker
|
|
339
|
+
# Validate payment type has balance tracker (v2 only)
|
|
254
340
|
try:
|
|
255
341
|
balance_tracker = marketplace.call("mapPaymentTypeBalanceTrackers", payment_type)
|
|
256
342
|
if balance_tracker == ZERO_ADDRESS:
|
|
257
|
-
|
|
343
|
+
# This is a validation failure for v2 - return False
|
|
344
|
+
logger.error(
|
|
345
|
+
f"No balance tracker for payment type 0x{payment_type.hex()}. "
|
|
346
|
+
"This is required for v2 marketplace requests."
|
|
347
|
+
)
|
|
258
348
|
return False
|
|
259
|
-
|
|
349
|
+
else:
|
|
350
|
+
logger.debug(f"Payment type balance tracker: {balance_tracker}")
|
|
260
351
|
except Exception as e:
|
|
261
|
-
|
|
352
|
+
# Check if this is a revert (v1 doesn't have this function) vs a network error
|
|
353
|
+
error_str = str(e).lower()
|
|
354
|
+
if "reverted" in error_str or "execution reverted" in error_str:
|
|
355
|
+
# v1 marketplaces don't have mapPaymentTypeBalanceTrackers - skip
|
|
356
|
+
logger.warning(
|
|
357
|
+
f"Could not validate payment type (marketplace may be v1): {e}. "
|
|
358
|
+
"Proceeding without validation."
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
# Real error - fail validation
|
|
362
|
+
logger.error(f"Failed to validate payment type (network error?): {e}")
|
|
363
|
+
return False
|
|
262
364
|
|
|
263
365
|
return True
|
|
264
366
|
|
|
@@ -269,7 +371,7 @@ class MechManagerMixin:
|
|
|
269
371
|
chain_name = self.chain_name if self.service else getattr(self, "chain_name", "gnosis")
|
|
270
372
|
protocol_contracts = OLAS_CONTRACTS.get(chain_name, {})
|
|
271
373
|
|
|
272
|
-
resolved_mp = marketplace_addr or protocol_contracts.get("
|
|
374
|
+
resolved_mp = marketplace_addr or protocol_contracts.get("OLAS_MECH_MARKETPLACE_V2")
|
|
273
375
|
if not resolved_mp:
|
|
274
376
|
raise ValueError(f"Mech Marketplace address not found for chain {chain_name}")
|
|
275
377
|
|
|
@@ -294,14 +396,30 @@ class MechManagerMixin:
|
|
|
294
396
|
self,
|
|
295
397
|
data: bytes,
|
|
296
398
|
value: Optional[int] = None,
|
|
297
|
-
marketplace_address: Optional[str] = None,
|
|
298
399
|
priority_mech: Optional[str] = None,
|
|
400
|
+
marketplace_address: Optional[str] = None,
|
|
299
401
|
max_delivery_rate: Optional[int] = None,
|
|
300
402
|
payment_type: Optional[bytes] = None,
|
|
301
403
|
payment_data: bytes = b"",
|
|
302
404
|
response_timeout: int = 300,
|
|
303
405
|
) -> Optional[str]:
|
|
304
|
-
"""Send a marketplace mech request with validation.
|
|
406
|
+
"""Send a marketplace mech request with validation.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
data: Request data payload (bytes).
|
|
410
|
+
value: Native currency value to send with request (wei).
|
|
411
|
+
priority_mech: Priority mech address for request processing.
|
|
412
|
+
marketplace_address: The marketplace contract address from activity checker.
|
|
413
|
+
If None, falls back to OLAS_MECH_MARKETPLACE_V2 constant.
|
|
414
|
+
max_delivery_rate: Maximum delivery rate for the mech.
|
|
415
|
+
payment_type: Payment type bytes32 (defaults to PAYMENT_TYPE_NATIVE).
|
|
416
|
+
payment_data: Additional payment data.
|
|
417
|
+
response_timeout: Timeout for response in seconds.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Transaction hash if successful, None otherwise.
|
|
421
|
+
|
|
422
|
+
"""
|
|
305
423
|
if not self.service:
|
|
306
424
|
logger.error("No active service")
|
|
307
425
|
return None
|
|
@@ -314,6 +432,17 @@ class MechManagerMixin:
|
|
|
314
432
|
logger.error(e)
|
|
315
433
|
return None
|
|
316
434
|
|
|
435
|
+
# Dispatch to v1 handler if marketplace is v1
|
|
436
|
+
if marketplace_address in V1_MARKETPLACES:
|
|
437
|
+
return self._send_v1_marketplace_request(
|
|
438
|
+
data=data,
|
|
439
|
+
marketplace_address=marketplace_address,
|
|
440
|
+
priority_mech=priority_mech,
|
|
441
|
+
response_timeout=response_timeout,
|
|
442
|
+
value=value,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# v2 flow
|
|
317
446
|
marketplace = MechMarketplaceContract(marketplace_address, chain_name=self.chain_name)
|
|
318
447
|
|
|
319
448
|
if not self._validate_priority_mech(marketplace, priority_mech):
|
|
@@ -350,6 +479,77 @@ class MechManagerMixin:
|
|
|
350
479
|
expected_event="MarketplaceRequest",
|
|
351
480
|
)
|
|
352
481
|
|
|
482
|
+
def _send_v1_marketplace_request(
|
|
483
|
+
self,
|
|
484
|
+
data: bytes,
|
|
485
|
+
marketplace_address: str,
|
|
486
|
+
priority_mech: str,
|
|
487
|
+
response_timeout: int = 300,
|
|
488
|
+
value: Optional[int] = None,
|
|
489
|
+
) -> Optional[str]:
|
|
490
|
+
"""Send a v1 marketplace mech request.
|
|
491
|
+
|
|
492
|
+
v1 marketplace (VERSION 1.0.0) requires staking instance and service ID
|
|
493
|
+
for both the mech and the requester, unlike v2 which uses payment types.
|
|
494
|
+
"""
|
|
495
|
+
if not self.service:
|
|
496
|
+
logger.error("No active service")
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
# Get mech info from DEFAULT_PRIORITY_MECH (now a 3-tuple)
|
|
500
|
+
mech_info = DEFAULT_PRIORITY_MECH.get(marketplace_address)
|
|
501
|
+
if not mech_info or len(mech_info) < 3:
|
|
502
|
+
logger.error(f"No priority mech info for v1 marketplace {marketplace_address}")
|
|
503
|
+
return None
|
|
504
|
+
|
|
505
|
+
priority_mech_address, priority_mech_service_id, priority_mech_staking = mech_info
|
|
506
|
+
|
|
507
|
+
if not priority_mech_staking:
|
|
508
|
+
logger.error(f"No mech staking instance for v1 marketplace {marketplace_address}")
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
# Get requester staking info from current service
|
|
512
|
+
requester_staking_instance = self.service.staking_contract_address
|
|
513
|
+
requester_service_id = self.service.service_id
|
|
514
|
+
|
|
515
|
+
if not requester_staking_instance:
|
|
516
|
+
logger.error("No staking contract for current service (required for v1)")
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
# Build v1 request params
|
|
520
|
+
params = V1RequestParams(
|
|
521
|
+
data=data,
|
|
522
|
+
priority_mech=priority_mech_address,
|
|
523
|
+
priority_mech_staking_instance=priority_mech_staking,
|
|
524
|
+
priority_mech_service_id=priority_mech_service_id,
|
|
525
|
+
requester_staking_instance=requester_staking_instance,
|
|
526
|
+
requester_service_id=requester_service_id,
|
|
527
|
+
response_timeout=response_timeout,
|
|
528
|
+
value=value or 10_000_000_000_000_000, # 0.01 xDAI default
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
logger.info(
|
|
532
|
+
f"[MECH-V1] Sending v1 marketplace request to {marketplace_address} "
|
|
533
|
+
f"(mech={priority_mech_address}, mech_svc={priority_mech_service_id})"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
marketplace = MechMarketplaceV1Contract(marketplace_address, chain_name=self.chain_name)
|
|
537
|
+
tx_data = marketplace.prepare_request_tx(
|
|
538
|
+
from_address=self.service.multisig_address,
|
|
539
|
+
params=params,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if not tx_data:
|
|
543
|
+
logger.error("Failed to prepare v1 marketplace request transaction")
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
return self._execute_mech_tx(
|
|
547
|
+
tx_data=tx_data,
|
|
548
|
+
to_address=str(marketplace_address),
|
|
549
|
+
contract_instance=marketplace,
|
|
550
|
+
expected_event="MarketplaceRequest",
|
|
551
|
+
)
|
|
552
|
+
|
|
353
553
|
def _execute_mech_tx(
|
|
354
554
|
self,
|
|
355
555
|
tx_data: dict,
|
|
@@ -412,6 +612,15 @@ class MechManagerMixin:
|
|
|
412
612
|
|
|
413
613
|
if event_found:
|
|
414
614
|
logger.info(f"Event '{expected_event}' verified successfully")
|
|
615
|
+
|
|
616
|
+
# Log transfer events from receipt
|
|
617
|
+
from iwa.core.services.transaction import TransferLogger
|
|
618
|
+
|
|
619
|
+
transfer_logger = TransferLogger(
|
|
620
|
+
self.wallet.account_service, self.registry.chain_interface
|
|
621
|
+
)
|
|
622
|
+
transfer_logger.log_transfers(receipt)
|
|
623
|
+
|
|
415
624
|
return tx_hash
|
|
416
625
|
else:
|
|
417
626
|
logger.error(f"Event '{expected_event}' NOT found in transaction logs")
|
|
@@ -50,6 +50,7 @@ from typing import Optional
|
|
|
50
50
|
from loguru import logger
|
|
51
51
|
from web3 import Web3
|
|
52
52
|
|
|
53
|
+
from iwa.core.contracts.cache import ContractCache
|
|
53
54
|
from iwa.core.types import EthereumAddress
|
|
54
55
|
from iwa.core.utils import get_tx_hash
|
|
55
56
|
from iwa.plugins.olas.contracts.staking import StakingContract, StakingState
|
|
@@ -96,7 +97,9 @@ class StakingManagerMixin:
|
|
|
96
97
|
|
|
97
98
|
# Load the staking contract
|
|
98
99
|
try:
|
|
99
|
-
staking =
|
|
100
|
+
staking = ContractCache().get_contract(
|
|
101
|
+
StakingContract, str(staking_address), chain_name=self.chain_name
|
|
102
|
+
)
|
|
100
103
|
except Exception as e:
|
|
101
104
|
logger.error(f"Failed to load staking contract: {e}")
|
|
102
105
|
return StakingStatus(
|
|
@@ -613,7 +616,8 @@ class StakingManagerMixin:
|
|
|
613
616
|
# Load staking contract if not provided
|
|
614
617
|
if not staking_contract:
|
|
615
618
|
try:
|
|
616
|
-
staking_contract =
|
|
619
|
+
staking_contract = ContractCache().get_contract(
|
|
620
|
+
StakingContract,
|
|
617
621
|
str(self.service.staking_contract_address),
|
|
618
622
|
chain_name=self.service.chain_name,
|
|
619
623
|
)
|
|
@@ -309,7 +309,10 @@ def test_importer_import_service_config_duplicate(mock_wallet):
|
|
|
309
309
|
|
|
310
310
|
def test_sm_create_token_utility_missing(mock_wallet):
|
|
311
311
|
"""Cover create() with missing token utility (lines 204-206)."""
|
|
312
|
-
with patch("iwa.core.models.Config")
|
|
312
|
+
with patch("iwa.core.models.Config"), \
|
|
313
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
314
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
315
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
313
316
|
sm = ServiceManager(mock_wallet)
|
|
314
317
|
|
|
315
318
|
with patch.dict("iwa.plugins.olas.service_manager.base.OLAS_CONTRACTS", {"unknown": {}}):
|
|
@@ -320,7 +323,10 @@ def test_sm_create_token_utility_missing(mock_wallet):
|
|
|
320
323
|
|
|
321
324
|
def test_sm_get_staking_status_staked_info_fail(mock_wallet):
|
|
322
325
|
"""Cover get_staking_status with STAKED but get_service_info fails (lines 843-854)."""
|
|
323
|
-
with patch("iwa.core.models.Config")
|
|
326
|
+
with patch("iwa.core.models.Config"), \
|
|
327
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
328
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
329
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
324
330
|
sm = ServiceManager(mock_wallet)
|
|
325
331
|
sm.service = Service(
|
|
326
332
|
service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
|
|
@@ -341,7 +347,10 @@ def test_sm_get_staking_status_staked_info_fail(mock_wallet):
|
|
|
341
347
|
|
|
342
348
|
def test_sm_call_checkpoint_prepare_fail(mock_wallet):
|
|
343
349
|
"""Cover call_checkpoint prepare failure (lines 1100-1102)."""
|
|
344
|
-
with patch("iwa.core.models.Config")
|
|
350
|
+
with patch("iwa.core.models.Config"), \
|
|
351
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
352
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
353
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
345
354
|
sm = ServiceManager(mock_wallet)
|
|
346
355
|
sm.service = Service(
|
|
347
356
|
service_name="t", chain_name="gnosis", service_id=1, staking_contract_address=VALID_ADDR
|
|
@@ -360,7 +369,10 @@ def test_sm_call_checkpoint_prepare_fail(mock_wallet):
|
|
|
360
369
|
|
|
361
370
|
def test_sm_spin_up_no_service(mock_wallet):
|
|
362
371
|
"""Cover spin_up with no service (lines 1167-1170)."""
|
|
363
|
-
with patch("iwa.core.models.Config")
|
|
372
|
+
with patch("iwa.core.models.Config"), \
|
|
373
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
374
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
375
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
364
376
|
sm = ServiceManager(mock_wallet)
|
|
365
377
|
sm.service = None
|
|
366
378
|
|
|
@@ -370,7 +382,10 @@ def test_sm_spin_up_no_service(mock_wallet):
|
|
|
370
382
|
|
|
371
383
|
def test_sm_spin_up_activation_fail(mock_wallet):
|
|
372
384
|
"""Cover spin_up activation failure (lines 1181-1183)."""
|
|
373
|
-
with patch("iwa.core.models.Config")
|
|
385
|
+
with patch("iwa.core.models.Config"), \
|
|
386
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
387
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
388
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
374
389
|
sm = ServiceManager(mock_wallet)
|
|
375
390
|
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
376
391
|
|
|
@@ -387,7 +402,10 @@ def test_sm_spin_up_activation_fail(mock_wallet):
|
|
|
387
402
|
|
|
388
403
|
def test_sm_wind_down_no_service(mock_wallet):
|
|
389
404
|
"""Cover wind_down with no service (lines 1264-1266)."""
|
|
390
|
-
with patch("iwa.core.models.Config")
|
|
405
|
+
with patch("iwa.core.models.Config"), \
|
|
406
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
407
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
408
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
391
409
|
sm = ServiceManager(mock_wallet)
|
|
392
410
|
sm.service = None
|
|
393
411
|
|
|
@@ -397,7 +415,10 @@ def test_sm_wind_down_no_service(mock_wallet):
|
|
|
397
415
|
|
|
398
416
|
def test_sm_wind_down_nonexistent(mock_wallet):
|
|
399
417
|
"""Cover wind_down with non-existent service (lines 1274-1276)."""
|
|
400
|
-
with patch("iwa.core.models.Config")
|
|
418
|
+
with patch("iwa.core.models.Config"), \
|
|
419
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
420
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
421
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
401
422
|
sm = ServiceManager(mock_wallet)
|
|
402
423
|
sm.service = Service(service_name="t", chain_name="gnosis", service_id=1)
|
|
403
424
|
|
|
@@ -411,7 +432,10 @@ def test_sm_wind_down_nonexistent(mock_wallet):
|
|
|
411
432
|
|
|
412
433
|
def test_sm_mech_request_no_service(mock_wallet):
|
|
413
434
|
"""Cover _send_legacy_mech_request with no service (lines 1502-1504)."""
|
|
414
|
-
with patch("iwa.core.models.Config")
|
|
435
|
+
with patch("iwa.core.models.Config"), \
|
|
436
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
437
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
438
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
415
439
|
sm = ServiceManager(mock_wallet)
|
|
416
440
|
sm.service = None
|
|
417
441
|
|
|
@@ -421,7 +445,10 @@ def test_sm_mech_request_no_service(mock_wallet):
|
|
|
421
445
|
|
|
422
446
|
def test_sm_mech_request_no_address(mock_wallet):
|
|
423
447
|
"""Cover _send_legacy_mech_request missing mech address (lines 1510-1512)."""
|
|
424
|
-
with patch("iwa.core.models.Config")
|
|
448
|
+
with patch("iwa.core.models.Config"), \
|
|
449
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
450
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
451
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
425
452
|
sm = ServiceManager(mock_wallet)
|
|
426
453
|
sm.service = Service(service_name="t", chain_name="unknown", service_id=1)
|
|
427
454
|
|
|
@@ -431,7 +458,8 @@ def test_sm_mech_request_no_address(mock_wallet):
|
|
|
431
458
|
|
|
432
459
|
def test_sm_marketplace_mech_no_service(mock_wallet):
|
|
433
460
|
"""Cover _send_marketplace_mech_request with no service (lines 1549-1551)."""
|
|
434
|
-
with patch("iwa.core.models.Config")
|
|
461
|
+
with patch("iwa.core.models.Config"), \
|
|
462
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache"):
|
|
435
463
|
sm = ServiceManager(mock_wallet)
|
|
436
464
|
sm.service = None
|
|
437
465
|
|
|
@@ -114,11 +114,16 @@ def service_manager(
|
|
|
114
114
|
mock_service,
|
|
115
115
|
):
|
|
116
116
|
"""ServiceManager fixture with mocked dependencies."""
|
|
117
|
-
with patch("iwa.plugins.olas.service_manager.base.Config") as local_mock_config
|
|
117
|
+
with patch("iwa.plugins.olas.service_manager.base.Config") as local_mock_config, \
|
|
118
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache:
|
|
118
119
|
instance = local_mock_config.return_value
|
|
119
120
|
instance.plugins = {"olas": mock_olas_config}
|
|
120
121
|
instance.save_config = MagicMock()
|
|
121
122
|
|
|
123
|
+
# Mock ContractCache to return MagicMock contracts
|
|
124
|
+
mock_cache_instance = mock_cache.return_value
|
|
125
|
+
mock_cache_instance.get_contract.return_value = MagicMock()
|
|
126
|
+
|
|
122
127
|
sm = ServiceManager(mock_wallet)
|
|
123
128
|
# Ensure service is properly set
|
|
124
129
|
sm.service = mock_service
|
|
@@ -127,6 +132,7 @@ def service_manager(
|
|
|
127
132
|
yield sm
|
|
128
133
|
|
|
129
134
|
|
|
135
|
+
|
|
130
136
|
def test_init(service_manager):
|
|
131
137
|
"""Test initialization."""
|
|
132
138
|
assert service_manager.registry is not None
|
|
@@ -23,12 +23,19 @@ def mock_wallet():
|
|
|
23
23
|
return wallet
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def mock_manager(mock_wallet):
|
|
27
28
|
"""Setup a ServiceManager with mocked dependencies."""
|
|
28
|
-
with patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls
|
|
29
|
+
with patch("iwa.plugins.olas.service_manager.base.Config") as mock_cfg_cls, \
|
|
30
|
+
patch("iwa.plugins.olas.service_manager.base.ContractCache") as mock_cache, \
|
|
31
|
+
patch("iwa.plugins.olas.service_manager.staking.ContractCache", mock_cache):
|
|
32
|
+
|
|
33
|
+
mock_cache.return_value.get_contract.side_effect = lambda cls, *a, **k: cls(*a, **k)
|
|
34
|
+
|
|
29
35
|
mock_cfg = mock_cfg_cls.return_value
|
|
30
36
|
mock_cfg.plugins = {"olas": MagicMock()}
|
|
31
37
|
mock_cfg.plugins["olas"].get_service.return_value = None
|
|
38
|
+
|
|
32
39
|
with patch(
|
|
33
40
|
"iwa.plugins.olas.service_manager.OLAS_CONTRACTS",
|
|
34
41
|
{
|
|
@@ -43,18 +50,22 @@ def setup_manager(mock_wallet):
|
|
|
43
50
|
mock_if = mock_if_cls.return_value
|
|
44
51
|
mock_if.get.return_value.chain.name.lower.return_value = "gnosis"
|
|
45
52
|
mock_if.get.return_value.get_contract_address.return_value = VALID_ADDR
|
|
53
|
+
|
|
46
54
|
manager = ServiceManager(mock_wallet)
|
|
47
55
|
manager.registry = MagicMock()
|
|
48
56
|
manager.manager_contract = MagicMock()
|
|
49
57
|
manager.olas_config = mock_cfg.plugins["olas"]
|
|
50
58
|
manager.chain_name = "gnosis"
|
|
59
|
+
# Fix recursive mock issue by setting explicit return value
|
|
60
|
+
manager.registry.chain_interface = MagicMock()
|
|
51
61
|
manager.registry.chain_interface.get_contract_address.return_value = VALID_ADDR
|
|
52
|
-
|
|
62
|
+
|
|
63
|
+
yield manager
|
|
53
64
|
|
|
54
65
|
|
|
55
|
-
def test_service_manager_mech_requests_failures(
|
|
66
|
+
def test_service_manager_mech_requests_failures(mock_manager):
|
|
56
67
|
"""Test failure paths in mech requests."""
|
|
57
|
-
manager =
|
|
68
|
+
manager = mock_manager
|
|
58
69
|
|
|
59
70
|
# Service missing
|
|
60
71
|
manager.service = None
|
|
@@ -99,9 +110,9 @@ def test_service_manager_mech_requests_failures(mock_wallet):
|
|
|
99
110
|
assert manager.send_mech_request(b"data", use_marketplace=False) is None
|
|
100
111
|
|
|
101
112
|
|
|
102
|
-
def test_service_manager_lifecycle_failures(mock_wallet):
|
|
113
|
+
def test_service_manager_lifecycle_failures(mock_manager, mock_wallet):
|
|
103
114
|
"""Test failure paths in lifecycle methods."""
|
|
104
|
-
manager =
|
|
115
|
+
manager = mock_manager
|
|
105
116
|
manager.service = Service(service_name="t", chain_name="gnosis", service_id=1, agent_ids=[1])
|
|
106
117
|
|
|
107
118
|
# register_agent failures
|
|
@@ -147,9 +158,9 @@ def test_service_manager_lifecycle_failures(mock_wallet):
|
|
|
147
158
|
assert manager.wind_down() is True
|
|
148
159
|
|
|
149
160
|
|
|
150
|
-
def test_service_manager_staking_status_failures(
|
|
161
|
+
def test_service_manager_staking_status_failures(mock_manager):
|
|
151
162
|
"""Test failure paths in get_staking_status."""
|
|
152
|
-
manager =
|
|
163
|
+
manager = mock_manager
|
|
153
164
|
|
|
154
165
|
# Service missing
|
|
155
166
|
manager.service = None
|
|
@@ -178,9 +189,9 @@ def test_service_manager_staking_status_failures(mock_wallet):
|
|
|
178
189
|
assert status.mech_requests_this_epoch == 0
|
|
179
190
|
|
|
180
191
|
|
|
181
|
-
def test_service_manager_verify_event_exception(
|
|
192
|
+
def test_service_manager_verify_event_exception(mock_manager):
|
|
182
193
|
"""Test exception path in _execute_mech_tx."""
|
|
183
|
-
manager =
|
|
194
|
+
manager = mock_manager
|
|
184
195
|
manager.service = Service(
|
|
185
196
|
service_name="t",
|
|
186
197
|
chain_name="gnosis",
|