iwa 0.0.0__py3-none-any.whl → 0.0.1a2__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.
- conftest.py +22 -0
- iwa/__init__.py +1 -0
- iwa/__main__.py +6 -0
- iwa/core/__init__.py +1 -0
- iwa/core/chain/__init__.py +68 -0
- iwa/core/chain/errors.py +47 -0
- iwa/core/chain/interface.py +514 -0
- iwa/core/chain/manager.py +38 -0
- iwa/core/chain/models.py +128 -0
- iwa/core/chain/rate_limiter.py +193 -0
- iwa/core/cli.py +210 -0
- iwa/core/constants.py +28 -0
- iwa/core/contracts/__init__.py +1 -0
- iwa/core/contracts/contract.py +297 -0
- iwa/core/contracts/erc20.py +79 -0
- iwa/core/contracts/multisend.py +71 -0
- iwa/core/db.py +317 -0
- iwa/core/keys.py +361 -0
- iwa/core/mnemonic.py +385 -0
- iwa/core/models.py +344 -0
- iwa/core/monitor.py +209 -0
- iwa/core/plugins.py +45 -0
- iwa/core/pricing.py +91 -0
- iwa/core/services/__init__.py +17 -0
- iwa/core/services/account.py +57 -0
- iwa/core/services/balance.py +113 -0
- iwa/core/services/plugin.py +88 -0
- iwa/core/services/safe.py +392 -0
- iwa/core/services/transaction.py +172 -0
- iwa/core/services/transfer/__init__.py +166 -0
- iwa/core/services/transfer/base.py +260 -0
- iwa/core/services/transfer/erc20.py +247 -0
- iwa/core/services/transfer/multisend.py +386 -0
- iwa/core/services/transfer/native.py +262 -0
- iwa/core/services/transfer/swap.py +326 -0
- iwa/core/settings.py +95 -0
- iwa/core/tables.py +60 -0
- iwa/core/test.py +27 -0
- iwa/core/tests/test_wallet.py +255 -0
- iwa/core/types.py +59 -0
- iwa/core/ui.py +99 -0
- iwa/core/utils.py +59 -0
- iwa/core/wallet.py +380 -0
- iwa/plugins/__init__.py +1 -0
- iwa/plugins/gnosis/__init__.py +5 -0
- iwa/plugins/gnosis/cow/__init__.py +6 -0
- iwa/plugins/gnosis/cow/quotes.py +148 -0
- iwa/plugins/gnosis/cow/swap.py +403 -0
- iwa/plugins/gnosis/cow/types.py +20 -0
- iwa/plugins/gnosis/cow_utils.py +44 -0
- iwa/plugins/gnosis/plugin.py +68 -0
- iwa/plugins/gnosis/safe.py +157 -0
- iwa/plugins/gnosis/tests/test_cow.py +227 -0
- iwa/plugins/gnosis/tests/test_safe.py +100 -0
- iwa/plugins/olas/__init__.py +5 -0
- iwa/plugins/olas/constants.py +106 -0
- iwa/plugins/olas/contracts/activity_checker.py +93 -0
- iwa/plugins/olas/contracts/base.py +10 -0
- iwa/plugins/olas/contracts/mech.py +49 -0
- iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
- iwa/plugins/olas/contracts/service.py +215 -0
- iwa/plugins/olas/contracts/staking.py +403 -0
- iwa/plugins/olas/importer.py +736 -0
- iwa/plugins/olas/mech_reference.py +135 -0
- iwa/plugins/olas/models.py +110 -0
- iwa/plugins/olas/plugin.py +243 -0
- iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
- iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
- iwa/plugins/olas/service_manager/__init__.py +60 -0
- iwa/plugins/olas/service_manager/base.py +113 -0
- iwa/plugins/olas/service_manager/drain.py +336 -0
- iwa/plugins/olas/service_manager/lifecycle.py +839 -0
- iwa/plugins/olas/service_manager/mech.py +322 -0
- iwa/plugins/olas/service_manager/staking.py +530 -0
- iwa/plugins/olas/tests/conftest.py +30 -0
- iwa/plugins/olas/tests/test_importer.py +128 -0
- iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
- iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
- iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
- iwa/plugins/olas/tests/test_olas_integration.py +561 -0
- iwa/plugins/olas/tests/test_olas_models.py +144 -0
- iwa/plugins/olas/tests/test_olas_view.py +258 -0
- iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
- iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
- iwa/plugins/olas/tests/test_plugin.py +70 -0
- iwa/plugins/olas/tests/test_plugin_full.py +212 -0
- iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
- iwa/plugins/olas/tests/test_service_manager.py +1065 -0
- iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
- iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
- iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
- iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
- iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
- iwa/plugins/olas/tests/test_service_staking.py +342 -0
- iwa/plugins/olas/tests/test_staking_integration.py +269 -0
- iwa/plugins/olas/tests/test_staking_validation.py +109 -0
- iwa/plugins/olas/tui/__init__.py +1 -0
- iwa/plugins/olas/tui/olas_view.py +952 -0
- iwa/tools/check_profile.py +67 -0
- iwa/tools/release.py +111 -0
- iwa/tools/reset_env.py +111 -0
- iwa/tools/reset_tenderly.py +362 -0
- iwa/tools/restore_backup.py +82 -0
- iwa/tui/__init__.py +1 -0
- iwa/tui/app.py +174 -0
- iwa/tui/modals/__init__.py +5 -0
- iwa/tui/modals/base.py +406 -0
- iwa/tui/rpc.py +63 -0
- iwa/tui/screens/__init__.py +1 -0
- iwa/tui/screens/wallets.py +749 -0
- iwa/tui/tests/test_app.py +125 -0
- iwa/tui/tests/test_rpc.py +139 -0
- iwa/tui/tests/test_wallets_refactor.py +30 -0
- iwa/tui/tests/test_widgets.py +123 -0
- iwa/tui/widgets/__init__.py +5 -0
- iwa/tui/widgets/base.py +100 -0
- iwa/tui/workers.py +42 -0
- iwa/web/dependencies.py +76 -0
- iwa/web/models.py +76 -0
- iwa/web/routers/accounts.py +115 -0
- iwa/web/routers/olas/__init__.py +24 -0
- iwa/web/routers/olas/admin.py +169 -0
- iwa/web/routers/olas/funding.py +135 -0
- iwa/web/routers/olas/general.py +29 -0
- iwa/web/routers/olas/services.py +378 -0
- iwa/web/routers/olas/staking.py +341 -0
- iwa/web/routers/state.py +65 -0
- iwa/web/routers/swap.py +617 -0
- iwa/web/routers/transactions.py +153 -0
- iwa/web/server.py +155 -0
- iwa/web/tests/test_web_endpoints.py +713 -0
- iwa/web/tests/test_web_olas.py +430 -0
- iwa/web/tests/test_web_swap.py +103 -0
- iwa-0.0.1a2.dist-info/METADATA +234 -0
- iwa-0.0.1a2.dist-info/RECORD +186 -0
- iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
- iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
- iwa-0.0.1a2.dist-info/top_level.txt +4 -0
- tests/legacy_cow.py +248 -0
- tests/legacy_safe.py +93 -0
- tests/legacy_transaction_retry_logic.py +51 -0
- tests/legacy_tui.py +440 -0
- tests/legacy_wallets_screen.py +554 -0
- tests/legacy_web.py +243 -0
- tests/test_account_service.py +120 -0
- tests/test_balance_service.py +186 -0
- tests/test_chain.py +490 -0
- tests/test_chain_interface.py +210 -0
- tests/test_cli.py +139 -0
- tests/test_contract.py +195 -0
- tests/test_db.py +180 -0
- tests/test_drain_coverage.py +174 -0
- tests/test_erc20.py +95 -0
- tests/test_gnosis_plugin.py +111 -0
- tests/test_keys.py +449 -0
- tests/test_legacy_wallet.py +1285 -0
- tests/test_main.py +13 -0
- tests/test_mnemonic.py +217 -0
- tests/test_modals.py +109 -0
- tests/test_models.py +213 -0
- tests/test_monitor.py +202 -0
- tests/test_multisend.py +84 -0
- tests/test_plugin_service.py +119 -0
- tests/test_pricing.py +143 -0
- tests/test_rate_limiter.py +199 -0
- tests/test_reset_tenderly.py +202 -0
- tests/test_rpc_view.py +73 -0
- tests/test_safe_coverage.py +139 -0
- tests/test_safe_service.py +168 -0
- tests/test_service_manager_integration.py +61 -0
- tests/test_service_manager_structure.py +31 -0
- tests/test_service_transaction.py +176 -0
- tests/test_staking_router.py +71 -0
- tests/test_staking_simple.py +31 -0
- tests/test_tables.py +76 -0
- tests/test_transaction_service.py +161 -0
- tests/test_transfer_multisend.py +179 -0
- tests/test_transfer_native.py +220 -0
- tests/test_transfer_security.py +93 -0
- tests/test_transfer_structure.py +37 -0
- tests/test_transfer_swap_unit.py +155 -0
- tests/test_ui_coverage.py +66 -0
- tests/test_utils.py +53 -0
- tests/test_workers.py +91 -0
- tools/verify_drain.py +183 -0
- __init__.py +0 -2
- hello.py +0 -6
- iwa-0.0.0.dist-info/METADATA +0 -10
- iwa-0.0.0.dist-info/RECORD +0 -6
- iwa-0.0.0.dist-info/top_level.txt +0 -2
- {iwa-0.0.0.dist-info → iwa-0.0.1a2.dist-info}/WHEEL +0 -0
iwa/web/models.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Shared request models for Web API."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AccountCreateRequest(BaseModel):
|
|
9
|
+
"""Request model for creating an EOA."""
|
|
10
|
+
|
|
11
|
+
tag: str = Field(description="Human-readable tag for the new account")
|
|
12
|
+
|
|
13
|
+
@field_validator("tag")
|
|
14
|
+
@classmethod
|
|
15
|
+
def validate_tag(cls, v: str) -> str:
|
|
16
|
+
"""Validate tag is not empty and alphanumeric."""
|
|
17
|
+
if not v or not v.strip():
|
|
18
|
+
raise ValueError("Tag cannot be empty")
|
|
19
|
+
if not v.replace("_", "").replace("-", "").isalnum():
|
|
20
|
+
raise ValueError("Tag contains invalid characters")
|
|
21
|
+
return v
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SafeCreateRequest(BaseModel):
|
|
25
|
+
"""Request model for creating a Safe."""
|
|
26
|
+
|
|
27
|
+
tag: str = Field(description="Human-readable tag for the Safe")
|
|
28
|
+
owners: List[str] = Field(description="List of owner addresses (checksummed or lowercase)")
|
|
29
|
+
threshold: int = Field(description="Required signatures threshold")
|
|
30
|
+
chains: List[str] = Field(default=["gnosis"], description="List of chains to deploy on")
|
|
31
|
+
|
|
32
|
+
@field_validator("owners")
|
|
33
|
+
@classmethod
|
|
34
|
+
def validate_owners(cls, v: List[str]) -> List[str]:
|
|
35
|
+
"""Validate owners list is not empty and contains valid addresses or tags."""
|
|
36
|
+
if not v:
|
|
37
|
+
raise ValueError("Owners list cannot be empty")
|
|
38
|
+
for owner in v:
|
|
39
|
+
# Accept both addresses (0x...) and tags (alphanumeric with _ and -)
|
|
40
|
+
if owner.startswith("0x"):
|
|
41
|
+
if len(owner) != 42:
|
|
42
|
+
raise ValueError(f"Invalid owner address: {owner}")
|
|
43
|
+
else:
|
|
44
|
+
# Tag format validation
|
|
45
|
+
if not owner.replace("_", "").replace("-", "").replace(" ", "").isalnum():
|
|
46
|
+
raise ValueError(f"Invalid owner tag: {owner}")
|
|
47
|
+
# Check for duplicates
|
|
48
|
+
if len(v) != len(set(v)):
|
|
49
|
+
raise ValueError("Duplicate owners not allowed")
|
|
50
|
+
return v
|
|
51
|
+
|
|
52
|
+
@field_validator("threshold")
|
|
53
|
+
@classmethod
|
|
54
|
+
def validate_threshold(cls, v: int, info) -> int:
|
|
55
|
+
"""Validate threshold is valid."""
|
|
56
|
+
if v < 1:
|
|
57
|
+
raise ValueError("Threshold must be at least 1")
|
|
58
|
+
# Access owners if available to validate threshold <= len(owners)
|
|
59
|
+
# Note: Pydantic V2 uses ValidationInfo, V1 uses 'values' dict. Assuming V2 based on usage.
|
|
60
|
+
# If 'owners' failed validation, it might not be in info.data
|
|
61
|
+
if info.data and "owners" in info.data:
|
|
62
|
+
owners = info.data["owners"]
|
|
63
|
+
if v > len(owners):
|
|
64
|
+
raise ValueError("Threshold cannot be greater than number of owners")
|
|
65
|
+
return v
|
|
66
|
+
|
|
67
|
+
@field_validator("chains")
|
|
68
|
+
@classmethod
|
|
69
|
+
def validate_chains(cls, v: List[str]) -> List[str]:
|
|
70
|
+
"""Validate chains list."""
|
|
71
|
+
if not v:
|
|
72
|
+
raise ValueError("Must specify at least one chain")
|
|
73
|
+
for chain in v:
|
|
74
|
+
if not chain.replace("-", "").isalnum():
|
|
75
|
+
raise ValueError(f"Invalid chain name: {chain}")
|
|
76
|
+
return v
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Accounts Router for Web API."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
7
|
+
from slowapi import Limiter
|
|
8
|
+
from slowapi.util import get_remote_address
|
|
9
|
+
|
|
10
|
+
from iwa.web.dependencies import verify_auth, wallet
|
|
11
|
+
from iwa.web.models import AccountCreateRequest, SafeCreateRequest
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
router = APIRouter(prefix="/api/accounts", tags=["accounts"])
|
|
15
|
+
|
|
16
|
+
# Rate limiter for this router
|
|
17
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get(
|
|
21
|
+
"",
|
|
22
|
+
summary="Get accounts",
|
|
23
|
+
description="Retrieve all stored accounts and their balances for the specified chain.",
|
|
24
|
+
)
|
|
25
|
+
def get_accounts(
|
|
26
|
+
chain: str = "gnosis",
|
|
27
|
+
tokens: str = None,
|
|
28
|
+
auth: bool = Depends(verify_auth),
|
|
29
|
+
):
|
|
30
|
+
"""Get all accounts and their balances for a specific chain."""
|
|
31
|
+
if not chain.replace("-", "").isalnum():
|
|
32
|
+
raise HTTPException(status_code=400, detail="Invalid chain name")
|
|
33
|
+
try:
|
|
34
|
+
# Parse tokens from query parameter or use defaults
|
|
35
|
+
if tokens:
|
|
36
|
+
token_names = [t.strip() for t in tokens.split(",") if t.strip()]
|
|
37
|
+
else:
|
|
38
|
+
token_names = ["native", "OLAS", "WXDAI", "USDC"]
|
|
39
|
+
|
|
40
|
+
accounts_data, balances = wallet.get_accounts_balances(chain, token_names)
|
|
41
|
+
|
|
42
|
+
# Merge data
|
|
43
|
+
result = []
|
|
44
|
+
for addr, data in accounts_data.items():
|
|
45
|
+
account_balances = balances.get(addr, {})
|
|
46
|
+
# Determine account type: if it has 'signers' attribute, it's a Safe
|
|
47
|
+
account_type = "Safe" if hasattr(data, "signers") else "EOA"
|
|
48
|
+
result.append(
|
|
49
|
+
{
|
|
50
|
+
"address": addr,
|
|
51
|
+
"tag": data.tag,
|
|
52
|
+
"type": account_type,
|
|
53
|
+
"balances": account_balances,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return result
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Error fetching accounts: {e}")
|
|
60
|
+
raise HTTPException(status_code=500, detail=str(e)) from None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.post(
|
|
64
|
+
"/eoa",
|
|
65
|
+
summary="Create EOA",
|
|
66
|
+
description="Create a new Externally Owned Account (EOA) with a unique tag.",
|
|
67
|
+
)
|
|
68
|
+
@limiter.limit("5/minute")
|
|
69
|
+
def create_eoa(request: Request, req: AccountCreateRequest, auth: bool = Depends(verify_auth)):
|
|
70
|
+
"""Create a new EOA account with the given tag."""
|
|
71
|
+
try:
|
|
72
|
+
wallet.key_storage.create_account(req.tag)
|
|
73
|
+
return {"status": "success"}
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@router.post(
|
|
79
|
+
"/safe",
|
|
80
|
+
summary="Create Safe",
|
|
81
|
+
description="Deploy a new Gnosis Safe multisig wallet on selected chains.",
|
|
82
|
+
)
|
|
83
|
+
@limiter.limit("3/minute")
|
|
84
|
+
def create_safe(request: Request, req: SafeCreateRequest, auth: bool = Depends(verify_auth)):
|
|
85
|
+
"""Create a new Safe multisig account."""
|
|
86
|
+
try:
|
|
87
|
+
# We use a timestamp-based salt to avoid collisions
|
|
88
|
+
salt_nonce = int(time.time() * 1000)
|
|
89
|
+
|
|
90
|
+
# Resolve owner tags to addresses
|
|
91
|
+
resolved_owners = []
|
|
92
|
+
for owner in req.owners:
|
|
93
|
+
if owner.startswith("0x"):
|
|
94
|
+
resolved_owners.append(owner)
|
|
95
|
+
else:
|
|
96
|
+
# It's a tag, resolve to address
|
|
97
|
+
account = wallet.account_service.resolve_account(owner)
|
|
98
|
+
if not account:
|
|
99
|
+
raise ValueError(f"Owner account not found: {owner}")
|
|
100
|
+
resolved_owners.append(account.address)
|
|
101
|
+
|
|
102
|
+
# Deploy on all requested chains
|
|
103
|
+
for chain_name in req.chains:
|
|
104
|
+
wallet.safe_service.create_safe(
|
|
105
|
+
"master", # WebUI uses master as deployer by default
|
|
106
|
+
resolved_owners,
|
|
107
|
+
req.threshold,
|
|
108
|
+
chain_name,
|
|
109
|
+
req.tag,
|
|
110
|
+
salt_nonce,
|
|
111
|
+
)
|
|
112
|
+
return {"status": "success"}
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Error creating Safe: {e}")
|
|
115
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Olas Router Package."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from iwa.core.models import Config
|
|
6
|
+
from iwa.plugins.olas.models import OlasConfig
|
|
7
|
+
from iwa.web.routers.olas.admin import router as admin_router
|
|
8
|
+
from iwa.web.routers.olas.funding import router as funding_router
|
|
9
|
+
from iwa.web.routers.olas.general import router as general_router
|
|
10
|
+
from iwa.web.routers.olas.services import router as services_router
|
|
11
|
+
from iwa.web.routers.olas.staking import router as staking_router
|
|
12
|
+
|
|
13
|
+
# Create main router
|
|
14
|
+
router = APIRouter(prefix="/api/olas", tags=["olas"])
|
|
15
|
+
|
|
16
|
+
# Include sub-routers directly without extra prefix/tags (already set in main or sub)
|
|
17
|
+
# Note: Sub-routers define their own endpoints relative to the main router root
|
|
18
|
+
router.include_router(general_router)
|
|
19
|
+
router.include_router(services_router)
|
|
20
|
+
router.include_router(staking_router)
|
|
21
|
+
router.include_router(funding_router)
|
|
22
|
+
router.include_router(admin_router)
|
|
23
|
+
|
|
24
|
+
__all__ = ["router", "Config", "OlasConfig"]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Olas Admin Router."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
6
|
+
from slowapi import Limiter
|
|
7
|
+
from slowapi.util import get_remote_address
|
|
8
|
+
|
|
9
|
+
from iwa.core.models import Config
|
|
10
|
+
from iwa.plugins.olas.models import OlasConfig
|
|
11
|
+
from iwa.web.dependencies import verify_auth, wallet
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
router = APIRouter(tags=["olas"])
|
|
15
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post(
|
|
19
|
+
"/activate/{service_key}",
|
|
20
|
+
summary="Activate Registration",
|
|
21
|
+
description="Activate registration for a service (step 1 after creation).",
|
|
22
|
+
)
|
|
23
|
+
def activate_registration(service_key: str, auth: bool = Depends(verify_auth)):
|
|
24
|
+
"""Activate service registration."""
|
|
25
|
+
try:
|
|
26
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
27
|
+
|
|
28
|
+
config = Config()
|
|
29
|
+
olas_config = OlasConfig.model_validate(config.plugins["olas"])
|
|
30
|
+
service = olas_config.services.get(service_key)
|
|
31
|
+
|
|
32
|
+
if not service:
|
|
33
|
+
raise HTTPException(status_code=404, detail="Service not found")
|
|
34
|
+
|
|
35
|
+
manager = ServiceManager(wallet)
|
|
36
|
+
manager.service = service
|
|
37
|
+
|
|
38
|
+
success = manager.activate_registration()
|
|
39
|
+
if success:
|
|
40
|
+
return {"status": "success"}
|
|
41
|
+
else:
|
|
42
|
+
raise HTTPException(status_code=400, detail="Failed to activate registration")
|
|
43
|
+
|
|
44
|
+
except HTTPException:
|
|
45
|
+
raise
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(f"Error activating registration: {e}")
|
|
48
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.post(
|
|
52
|
+
"/register/{service_key}",
|
|
53
|
+
summary="Register Agent",
|
|
54
|
+
description="Register an agent for the service (step 2 after activation).",
|
|
55
|
+
)
|
|
56
|
+
def register_agent(service_key: str, auth: bool = Depends(verify_auth)):
|
|
57
|
+
"""Register agent for service."""
|
|
58
|
+
try:
|
|
59
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
60
|
+
|
|
61
|
+
config = Config()
|
|
62
|
+
olas_config = OlasConfig.model_validate(config.plugins["olas"])
|
|
63
|
+
service = olas_config.services.get(service_key)
|
|
64
|
+
|
|
65
|
+
if not service:
|
|
66
|
+
raise HTTPException(status_code=404, detail="Service not found")
|
|
67
|
+
|
|
68
|
+
manager = ServiceManager(wallet)
|
|
69
|
+
manager.service = service
|
|
70
|
+
|
|
71
|
+
success = manager.register_agent()
|
|
72
|
+
if success:
|
|
73
|
+
return {"status": "success"}
|
|
74
|
+
else:
|
|
75
|
+
raise HTTPException(status_code=400, detail="Failed to register agent")
|
|
76
|
+
|
|
77
|
+
except HTTPException:
|
|
78
|
+
raise
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Error registering agent: {e}")
|
|
81
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.post(
|
|
85
|
+
"/deploy-step/{service_key}",
|
|
86
|
+
summary="Deploy Service (Step 3)",
|
|
87
|
+
description="Deploy the service (step 3, creates multisig Safe).",
|
|
88
|
+
)
|
|
89
|
+
def deploy_service_step(service_key: str, auth: bool = Depends(verify_auth)):
|
|
90
|
+
"""Deploy the service."""
|
|
91
|
+
try:
|
|
92
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
93
|
+
|
|
94
|
+
config = Config()
|
|
95
|
+
olas_config = OlasConfig.model_validate(config.plugins["olas"])
|
|
96
|
+
service = olas_config.services.get(service_key)
|
|
97
|
+
|
|
98
|
+
if not service:
|
|
99
|
+
raise HTTPException(status_code=404, detail="Service not found")
|
|
100
|
+
|
|
101
|
+
manager = ServiceManager(wallet)
|
|
102
|
+
manager.service = service
|
|
103
|
+
|
|
104
|
+
success = manager.deploy()
|
|
105
|
+
if success:
|
|
106
|
+
return {"status": "success"}
|
|
107
|
+
else:
|
|
108
|
+
raise HTTPException(status_code=400, detail="Failed to deploy service")
|
|
109
|
+
|
|
110
|
+
except HTTPException:
|
|
111
|
+
raise
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Error deploying service: {e}")
|
|
114
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.post(
|
|
118
|
+
"/terminate/{service_key}",
|
|
119
|
+
summary="Terminate Service",
|
|
120
|
+
description="Wind down a service: unstake (if staked) → terminate → unbond.",
|
|
121
|
+
)
|
|
122
|
+
@limiter.limit("3/minute")
|
|
123
|
+
def terminate_service(request: Request, service_key: str, auth: bool = Depends(verify_auth)):
|
|
124
|
+
"""Terminate and unbond a service using wind_down."""
|
|
125
|
+
try:
|
|
126
|
+
from iwa.plugins.olas.contracts.staking import StakingContract
|
|
127
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
128
|
+
|
|
129
|
+
config = Config()
|
|
130
|
+
olas_config = OlasConfig.model_validate(config.plugins["olas"])
|
|
131
|
+
service = olas_config.services.get(service_key)
|
|
132
|
+
|
|
133
|
+
if not service:
|
|
134
|
+
raise HTTPException(status_code=404, detail="Service not found")
|
|
135
|
+
|
|
136
|
+
manager = ServiceManager(wallet)
|
|
137
|
+
manager.service = service
|
|
138
|
+
|
|
139
|
+
# Get current state for logging
|
|
140
|
+
current_state = manager.get_service_state()
|
|
141
|
+
logger.info(f"[WIND_DOWN] Service {service_key} state: {current_state}")
|
|
142
|
+
|
|
143
|
+
if current_state == "PRE_REGISTRATION":
|
|
144
|
+
return {"status": "success", "message": "Service already in PRE_REGISTRATION state"}
|
|
145
|
+
|
|
146
|
+
if current_state == "NON_EXISTENT":
|
|
147
|
+
raise HTTPException(status_code=400, detail="Service does not exist")
|
|
148
|
+
|
|
149
|
+
# Prepare staking contract if service is staked
|
|
150
|
+
staking_contract = None
|
|
151
|
+
if service.staking_contract_address:
|
|
152
|
+
staking_contract = StakingContract(service.staking_contract_address, service.chain_name)
|
|
153
|
+
|
|
154
|
+
# Use wind_down which handles unstake → terminate → unbond
|
|
155
|
+
success = manager.wind_down(staking_contract=staking_contract)
|
|
156
|
+
|
|
157
|
+
if success:
|
|
158
|
+
return {"status": "success", "message": "Service wound down to PRE_REGISTRATION"}
|
|
159
|
+
else:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=400,
|
|
162
|
+
detail="Wind down failed. Check logs for details.",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
except HTTPException:
|
|
166
|
+
raise
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Error winding down service: {e}")
|
|
169
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Olas Funding Router."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from slowapi import Limiter
|
|
8
|
+
from slowapi.util import get_remote_address
|
|
9
|
+
|
|
10
|
+
from iwa.core.models import Config
|
|
11
|
+
from iwa.plugins.olas.models import OlasConfig
|
|
12
|
+
from iwa.web.dependencies import verify_auth, wallet
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
router = APIRouter(tags=["olas"])
|
|
16
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FundRequest(BaseModel):
|
|
20
|
+
"""Request model for funding a service."""
|
|
21
|
+
|
|
22
|
+
agent_amount_eth: float = Field(default=0, description="Amount to fund agent in ETH")
|
|
23
|
+
safe_amount_eth: float = Field(default=0, description="Amount to fund safe in ETH")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.post(
|
|
27
|
+
"/fund/{service_key}",
|
|
28
|
+
summary="Fund Service",
|
|
29
|
+
description="Fund a service's agent and safe accounts with native currency.",
|
|
30
|
+
)
|
|
31
|
+
def fund_service(service_key: str, req: FundRequest, auth: bool = Depends(verify_auth)):
|
|
32
|
+
"""Fund a service's agent and safe accounts."""
|
|
33
|
+
try:
|
|
34
|
+
from web3 import Web3
|
|
35
|
+
|
|
36
|
+
config = Config()
|
|
37
|
+
olas_config = OlasConfig.model_validate(config.plugins["olas"])
|
|
38
|
+
service = olas_config.services.get(service_key)
|
|
39
|
+
|
|
40
|
+
if not service:
|
|
41
|
+
raise HTTPException(status_code=404, detail="Service not found")
|
|
42
|
+
|
|
43
|
+
funded = {}
|
|
44
|
+
|
|
45
|
+
# Fund agent if amount provided and agent exists
|
|
46
|
+
if req.agent_amount_eth > 0 and service.agent_address:
|
|
47
|
+
amount_wei = Web3.to_wei(req.agent_amount_eth, "ether")
|
|
48
|
+
tx_hash = wallet.send(
|
|
49
|
+
from_address_or_tag="master",
|
|
50
|
+
to_address_or_tag=service.agent_address,
|
|
51
|
+
amount_wei=amount_wei,
|
|
52
|
+
token_address_or_name="native",
|
|
53
|
+
chain_name=service.chain_name,
|
|
54
|
+
)
|
|
55
|
+
funded["agent"] = {"amount": req.agent_amount_eth, "tx_hash": tx_hash}
|
|
56
|
+
|
|
57
|
+
# Fund safe if amount provided and safe exists
|
|
58
|
+
if req.safe_amount_eth > 0 and service.multisig_address:
|
|
59
|
+
amount_wei = Web3.to_wei(req.safe_amount_eth, "ether")
|
|
60
|
+
tx_hash = wallet.send(
|
|
61
|
+
from_address_or_tag="master",
|
|
62
|
+
to_address_or_tag=str(service.multisig_address),
|
|
63
|
+
amount_wei=amount_wei,
|
|
64
|
+
token_address_or_name="native",
|
|
65
|
+
chain_name=service.chain_name,
|
|
66
|
+
)
|
|
67
|
+
funded["safe"] = {"amount": req.safe_amount_eth, "tx_hash": tx_hash}
|
|
68
|
+
|
|
69
|
+
if not funded:
|
|
70
|
+
raise HTTPException(
|
|
71
|
+
status_code=400, detail="No valid accounts to fund or amounts are zero"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return {"status": "success", "funded": funded}
|
|
75
|
+
|
|
76
|
+
except HTTPException:
|
|
77
|
+
raise
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Error funding service: {e}")
|
|
80
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post(
|
|
84
|
+
"/drain/{service_key}",
|
|
85
|
+
summary="Drain Service",
|
|
86
|
+
description="Drain all funds from a service's accounts to the master account.",
|
|
87
|
+
)
|
|
88
|
+
@limiter.limit("3/minute")
|
|
89
|
+
def drain_service(request: Request, service_key: str, auth: bool = Depends(verify_auth)):
|
|
90
|
+
"""Drain all funds from a service's accounts."""
|
|
91
|
+
try:
|
|
92
|
+
from iwa.plugins.olas.service_manager import ServiceManager
|
|
93
|
+
|
|
94
|
+
config = Config()
|
|
95
|
+
olas_config = OlasConfig.model_validate(config.plugins["olas"])
|
|
96
|
+
service = olas_config.services.get(service_key)
|
|
97
|
+
|
|
98
|
+
if not service:
|
|
99
|
+
raise HTTPException(status_code=404, detail="Service not found")
|
|
100
|
+
|
|
101
|
+
manager = ServiceManager(wallet)
|
|
102
|
+
manager.service = service
|
|
103
|
+
|
|
104
|
+
logger.info(f"[DRAIN] Starting drain for service {service_key}")
|
|
105
|
+
logger.info(f"[DRAIN] Agent: {service.agent_address}")
|
|
106
|
+
logger.info(f"[DRAIN] Safe: {service.multisig_address}")
|
|
107
|
+
logger.info(f"[DRAIN] Owner: {service.service_owner_address}")
|
|
108
|
+
|
|
109
|
+
# Drain all accounts (Safe, Agent, Owner)
|
|
110
|
+
try:
|
|
111
|
+
drained = manager.drain_service()
|
|
112
|
+
logger.info(f"[DRAIN] drain_service returned: {drained}")
|
|
113
|
+
except Exception as drain_ex:
|
|
114
|
+
logger.error(f"[DRAIN] drain_service threw exception: {drain_ex}")
|
|
115
|
+
import traceback
|
|
116
|
+
|
|
117
|
+
logger.error(f"[DRAIN] Traceback: {traceback.format_exc()}")
|
|
118
|
+
raise HTTPException(status_code=400, detail=str(drain_ex)) from drain_ex
|
|
119
|
+
|
|
120
|
+
if not drained:
|
|
121
|
+
raise HTTPException(
|
|
122
|
+
status_code=400,
|
|
123
|
+
detail="Nothing drained. Accounts may have no balance or private keys may be missing.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"status": "success",
|
|
128
|
+
"drained": drained,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
except HTTPException:
|
|
132
|
+
raise
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"Error draining service: {e}")
|
|
135
|
+
raise HTTPException(status_code=400, detail=str(e)) from None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Olas General Router."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends
|
|
6
|
+
|
|
7
|
+
from iwa.web.dependencies import verify_auth
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
router = APIRouter(tags=["olas"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get(
|
|
14
|
+
"/price",
|
|
15
|
+
summary="Get OLAS Price",
|
|
16
|
+
description="Get the current price of OLAS token in EUR from CoinGecko.",
|
|
17
|
+
)
|
|
18
|
+
def get_olas_price(auth: bool = Depends(verify_auth)):
|
|
19
|
+
"""Get current OLAS token price in EUR from CoinGecko."""
|
|
20
|
+
try:
|
|
21
|
+
from iwa.core.pricing import PriceService
|
|
22
|
+
|
|
23
|
+
price_service = PriceService()
|
|
24
|
+
price = price_service.get_token_price("autonolas", "eur")
|
|
25
|
+
|
|
26
|
+
return {"price_eur": price, "symbol": "OLAS"}
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logger.error(f"Error fetching OLAS price: {e}")
|
|
29
|
+
return {"price_eur": None, "symbol": "OLAS", "error": str(e)}
|