iwa 0.1.4__py3-none-any.whl → 0.1.6__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/plugins/olas/service_manager/base.py +26 -7
- iwa/plugins/olas/service_manager/drain.py +12 -0
- iwa/plugins/olas/service_manager/lifecycle.py +11 -0
- iwa/plugins/olas/service_manager/staking.py +36 -2
- iwa/plugins/olas/tests/test_service_manager_errors.py +3 -3
- iwa/plugins/olas/tests/test_service_staking.py +1 -1
- iwa/web/cache.py +143 -0
- iwa/web/routers/accounts.py +55 -27
- iwa/web/routers/olas/services.py +109 -41
- iwa/web/routers/olas/staking.py +4 -0
- iwa/web/server.py +1 -0
- iwa/web/tests/test_response_cache.py +660 -0
- {iwa-0.1.4.dist-info → iwa-0.1.6.dist-info}/METADATA +1 -1
- {iwa-0.1.4.dist-info → iwa-0.1.6.dist-info}/RECORD +18 -16
- {iwa-0.1.4.dist-info → iwa-0.1.6.dist-info}/WHEEL +0 -0
- {iwa-0.1.4.dist-info → iwa-0.1.6.dist-info}/entry_points.txt +0 -0
- {iwa-0.1.4.dist-info → iwa-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {iwa-0.1.4.dist-info → iwa-0.1.6.dist-info}/top_level.txt +0 -0
iwa/web/routers/olas/services.py
CHANGED
|
@@ -8,6 +8,7 @@ from pydantic import BaseModel, Field
|
|
|
8
8
|
|
|
9
9
|
from iwa.core.models import Config
|
|
10
10
|
from iwa.plugins.olas.models import OlasConfig
|
|
11
|
+
from iwa.web.cache import CacheTTL, response_cache
|
|
11
12
|
from iwa.web.dependencies import verify_auth, wallet
|
|
12
13
|
|
|
13
14
|
router = APIRouter(tags=["olas"])
|
|
@@ -140,6 +141,11 @@ def create_service(req: CreateServiceRequest, auth: bool = Depends(verify_auth))
|
|
|
140
141
|
# Get final state
|
|
141
142
|
final_state = manager.get_service_state()
|
|
142
143
|
|
|
144
|
+
# Invalidate caches for services list
|
|
145
|
+
response_cache.invalidate("service_state:")
|
|
146
|
+
response_cache.invalidate("staking_status:")
|
|
147
|
+
response_cache.invalidate("balances:")
|
|
148
|
+
|
|
143
149
|
return {
|
|
144
150
|
"status": "success",
|
|
145
151
|
"service_id": service_id,
|
|
@@ -220,6 +226,12 @@ def deploy_service(
|
|
|
220
226
|
)
|
|
221
227
|
|
|
222
228
|
final_state = manager.get_service_state()
|
|
229
|
+
|
|
230
|
+
# Invalidate caches for this service
|
|
231
|
+
response_cache.invalidate(f"service_state:{service_key}")
|
|
232
|
+
response_cache.invalidate(f"staking_status:{service_key}")
|
|
233
|
+
response_cache.invalidate(f"balances:{service_key}")
|
|
234
|
+
|
|
223
235
|
return {
|
|
224
236
|
"status": "success",
|
|
225
237
|
"service_key": service_key,
|
|
@@ -303,13 +315,63 @@ def _resolve_service_balances(service, chain: str) -> dict:
|
|
|
303
315
|
return balances
|
|
304
316
|
|
|
305
317
|
|
|
318
|
+
def _get_balances_cached(
|
|
319
|
+
service_key: str, service, chain: str, force_refresh: bool = False
|
|
320
|
+
) -> dict:
|
|
321
|
+
"""Get service balances with caching to prevent excessive RPC calls."""
|
|
322
|
+
cache_key = f"balances:{service_key}:{chain}"
|
|
323
|
+
|
|
324
|
+
if force_refresh:
|
|
325
|
+
response_cache.invalidate(cache_key)
|
|
326
|
+
|
|
327
|
+
def fetch_balances():
|
|
328
|
+
return _resolve_service_balances(service, chain)
|
|
329
|
+
|
|
330
|
+
return response_cache.get_or_compute(
|
|
331
|
+
cache_key, fetch_balances, CacheTTL.BALANCES
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _staking_status_to_dict(status) -> Optional[dict]:
|
|
336
|
+
"""Convert StakingStatus dataclass to dict for JSON serialization."""
|
|
337
|
+
if not status:
|
|
338
|
+
return None
|
|
339
|
+
return {
|
|
340
|
+
"is_staked": status.is_staked,
|
|
341
|
+
"staking_state": status.staking_state,
|
|
342
|
+
"staking_contract_address": status.staking_contract_address,
|
|
343
|
+
"staking_contract_name": status.staking_contract_name,
|
|
344
|
+
"accrued_reward_olas": status.accrued_reward_olas,
|
|
345
|
+
"accrued_reward_wei": status.accrued_reward_wei,
|
|
346
|
+
"epoch_number": status.epoch_number,
|
|
347
|
+
"epoch_end_utc": status.epoch_end_utc,
|
|
348
|
+
"remaining_epoch_seconds": status.remaining_epoch_seconds,
|
|
349
|
+
"mech_requests_this_epoch": status.mech_requests_this_epoch,
|
|
350
|
+
"required_mech_requests": status.required_mech_requests,
|
|
351
|
+
"has_enough_requests": status.has_enough_requests,
|
|
352
|
+
"liveness_ratio_passed": status.liveness_ratio_passed,
|
|
353
|
+
"unstake_available_at": status.unstake_available_at,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
306
357
|
@router.get(
|
|
307
358
|
"/services/basic",
|
|
308
359
|
summary="Get Basic Services",
|
|
309
|
-
description="Get a lightweight list of configured Olas services
|
|
360
|
+
description="Get a lightweight list of configured Olas services.",
|
|
310
361
|
)
|
|
311
|
-
def get_olas_services_basic(
|
|
312
|
-
|
|
362
|
+
def get_olas_services_basic(
|
|
363
|
+
chain: str = "gnosis",
|
|
364
|
+
refresh: bool = False,
|
|
365
|
+
auth: bool = Depends(verify_auth),
|
|
366
|
+
):
|
|
367
|
+
"""Get basic Olas service info from config.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
chain: Chain name to filter services.
|
|
371
|
+
refresh: If true, bypass cache and fetch fresh data.
|
|
372
|
+
auth: Authentication dependency (injected).
|
|
373
|
+
|
|
374
|
+
"""
|
|
313
375
|
if not chain.replace("-", "").isalnum():
|
|
314
376
|
raise HTTPException(status_code=400, detail="Invalid chain name")
|
|
315
377
|
|
|
@@ -327,14 +389,15 @@ def get_olas_services_basic(chain: str = "gnosis", auth: bool = Depends(verify_a
|
|
|
327
389
|
if service.chain_name != chain:
|
|
328
390
|
continue
|
|
329
391
|
|
|
330
|
-
# Get service state
|
|
331
|
-
state = "UNKNOWN"
|
|
392
|
+
# Get service state using ServiceManager's built-in caching
|
|
332
393
|
try:
|
|
333
394
|
manager = ServiceManager(wallet)
|
|
334
395
|
manager.service = service
|
|
335
|
-
|
|
396
|
+
manager._init_contracts(service.chain_name)
|
|
397
|
+
state = manager.get_service_state(force_refresh=refresh)
|
|
336
398
|
except Exception as e:
|
|
337
399
|
logger.warning(f"Could not get state for {service_key}: {e}")
|
|
400
|
+
state = "UNKNOWN"
|
|
338
401
|
|
|
339
402
|
# Get tags from wallet storage (fast, local lookup)
|
|
340
403
|
accounts = _resolve_service_accounts(service)
|
|
@@ -367,8 +430,19 @@ def get_olas_services_basic(chain: str = "gnosis", auth: bool = Depends(verify_a
|
|
|
367
430
|
summary="Get Service Details",
|
|
368
431
|
description="Get detailed status, balances, and staking info for a specific Olas service.",
|
|
369
432
|
)
|
|
370
|
-
def get_olas_service_details(
|
|
371
|
-
|
|
433
|
+
def get_olas_service_details(
|
|
434
|
+
service_key: str,
|
|
435
|
+
refresh: bool = False,
|
|
436
|
+
auth: bool = Depends(verify_auth),
|
|
437
|
+
):
|
|
438
|
+
"""Get full details for a single Olas service (staking, balances).
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
service_key: The service key (chain:id format).
|
|
442
|
+
refresh: If true, bypass cache and fetch fresh data.
|
|
443
|
+
auth: Authentication dependency (injected).
|
|
444
|
+
|
|
445
|
+
"""
|
|
372
446
|
try:
|
|
373
447
|
from iwa.plugins.olas.service_manager import ServiceManager
|
|
374
448
|
|
|
@@ -378,43 +452,28 @@ def get_olas_service_details(service_key: str, auth: bool = Depends(verify_auth)
|
|
|
378
452
|
|
|
379
453
|
olas_config = OlasConfig.model_validate(config.plugins["olas"])
|
|
380
454
|
if service_key not in olas_config.services:
|
|
381
|
-
raise HTTPException(
|
|
455
|
+
raise HTTPException(
|
|
456
|
+
status_code=404, detail=f"Service '{service_key}' not found"
|
|
457
|
+
)
|
|
382
458
|
|
|
383
459
|
service = olas_config.services[service_key]
|
|
384
460
|
chain = service.chain_name
|
|
385
461
|
|
|
462
|
+
# Use ServiceManager with built-in caching
|
|
386
463
|
manager = ServiceManager(wallet)
|
|
387
464
|
manager.service = service
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
staking = None
|
|
395
|
-
if staking_status:
|
|
396
|
-
staking = {
|
|
397
|
-
"is_staked": staking_status.is_staked,
|
|
398
|
-
"staking_state": staking_status.staking_state,
|
|
399
|
-
"staking_contract_address": staking_status.staking_contract_address,
|
|
400
|
-
"staking_contract_name": staking_status.staking_contract_name,
|
|
401
|
-
"accrued_reward_olas": staking_status.accrued_reward_olas,
|
|
402
|
-
"accrued_reward_wei": staking_status.accrued_reward_wei,
|
|
403
|
-
"epoch_number": staking_status.epoch_number,
|
|
404
|
-
"epoch_end_utc": staking_status.epoch_end_utc,
|
|
405
|
-
"remaining_epoch_seconds": staking_status.remaining_epoch_seconds,
|
|
406
|
-
"mech_requests_this_epoch": staking_status.mech_requests_this_epoch,
|
|
407
|
-
"required_mech_requests": staking_status.required_mech_requests,
|
|
408
|
-
"has_enough_requests": staking_status.has_enough_requests,
|
|
409
|
-
"liveness_ratio_passed": staking_status.liveness_ratio_passed,
|
|
410
|
-
"unstake_available_at": staking_status.unstake_available_at,
|
|
411
|
-
}
|
|
465
|
+
manager._init_contracts(service.chain_name)
|
|
466
|
+
|
|
467
|
+
# Get data with force_refresh if requested
|
|
468
|
+
service_state = manager.get_service_state(force_refresh=refresh)
|
|
469
|
+
staking_status = manager.get_staking_status(force_refresh=refresh)
|
|
470
|
+
balances = _get_balances_cached(service_key, service, chain, refresh)
|
|
412
471
|
|
|
413
472
|
return {
|
|
414
473
|
"key": service_key,
|
|
415
474
|
"state": service_state,
|
|
416
475
|
"accounts": balances,
|
|
417
|
-
"staking":
|
|
476
|
+
"staking": _staking_status_to_dict(staking_status),
|
|
418
477
|
}
|
|
419
478
|
|
|
420
479
|
except HTTPException:
|
|
@@ -427,21 +486,30 @@ def get_olas_service_details(service_key: str, auth: bool = Depends(verify_auth)
|
|
|
427
486
|
@router.get(
|
|
428
487
|
"/services",
|
|
429
488
|
summary="Get All Services",
|
|
430
|
-
description="Get comprehensive list of Olas services with full details
|
|
489
|
+
description="Get comprehensive list of Olas services with full details.",
|
|
431
490
|
)
|
|
432
|
-
def get_olas_services(
|
|
433
|
-
|
|
491
|
+
def get_olas_services(
|
|
492
|
+
chain: str = "gnosis",
|
|
493
|
+
refresh: bool = False,
|
|
494
|
+
auth: bool = Depends(verify_auth),
|
|
495
|
+
):
|
|
496
|
+
"""Get all Olas services with staking status for a specific chain.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
chain: Chain name to filter services.
|
|
500
|
+
refresh: If true, bypass cache and fetch fresh data.
|
|
501
|
+
auth: Authentication dependency (injected).
|
|
502
|
+
|
|
503
|
+
"""
|
|
434
504
|
if not chain.replace("-", "").isalnum():
|
|
435
505
|
raise HTTPException(status_code=400, detail="Invalid chain name")
|
|
436
506
|
|
|
437
507
|
try:
|
|
438
|
-
|
|
439
|
-
# Ideally we refactor this to be more efficient bulk query later
|
|
440
|
-
basic = get_olas_services_basic(chain, auth)
|
|
508
|
+
basic = get_olas_services_basic(chain, refresh, auth)
|
|
441
509
|
result = []
|
|
442
510
|
for svc in basic:
|
|
443
511
|
try:
|
|
444
|
-
details = get_olas_service_details(svc["key"], auth)
|
|
512
|
+
details = get_olas_service_details(svc["key"], refresh, auth)
|
|
445
513
|
# Merge details into basic info
|
|
446
514
|
svc["staking"] = details["staking"]
|
|
447
515
|
svc["accounts"] = details["accounts"]
|
iwa/web/routers/olas/staking.py
CHANGED
|
@@ -253,6 +253,7 @@ def stake_service(
|
|
|
253
253
|
success = manager.stake(staking)
|
|
254
254
|
|
|
255
255
|
if success:
|
|
256
|
+
# Cache invalidation handled by ServiceManager.stake()
|
|
256
257
|
return {"status": "success"}
|
|
257
258
|
else:
|
|
258
259
|
raise HTTPException(status_code=400, detail="Failed to stake service")
|
|
@@ -286,6 +287,7 @@ def claim_rewards(service_key: str, auth: bool = Depends(verify_auth)):
|
|
|
286
287
|
|
|
287
288
|
success, amount = manager.claim_rewards()
|
|
288
289
|
if success:
|
|
290
|
+
# Cache invalidation handled by ServiceManager.claim_rewards()
|
|
289
291
|
return {"status": "success", "amount": amount}
|
|
290
292
|
else:
|
|
291
293
|
raise HTTPException(status_code=400, detail="Failed to claim rewards")
|
|
@@ -323,6 +325,7 @@ def unstake_service(service_key: str, auth: bool = Depends(verify_auth)):
|
|
|
323
325
|
|
|
324
326
|
success = manager.unstake(staking_contract)
|
|
325
327
|
if success:
|
|
328
|
+
# Cache invalidation handled by ServiceManager.unstake()
|
|
326
329
|
return {"status": "success"}
|
|
327
330
|
else:
|
|
328
331
|
raise HTTPException(status_code=400, detail="Failed to unstake")
|
|
@@ -359,6 +362,7 @@ def checkpoint_service(service_key: str, auth: bool = Depends(verify_auth)):
|
|
|
359
362
|
|
|
360
363
|
success = manager.call_checkpoint(staking_contract)
|
|
361
364
|
if success:
|
|
365
|
+
# Cache invalidation handled by ServiceManager.call_checkpoint()
|
|
362
366
|
return {"status": "success"}
|
|
363
367
|
else:
|
|
364
368
|
# Check if it was just not needed
|