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.
Files changed (191) hide show
  1. conftest.py +22 -0
  2. iwa/__init__.py +1 -0
  3. iwa/__main__.py +6 -0
  4. iwa/core/__init__.py +1 -0
  5. iwa/core/chain/__init__.py +68 -0
  6. iwa/core/chain/errors.py +47 -0
  7. iwa/core/chain/interface.py +514 -0
  8. iwa/core/chain/manager.py +38 -0
  9. iwa/core/chain/models.py +128 -0
  10. iwa/core/chain/rate_limiter.py +193 -0
  11. iwa/core/cli.py +210 -0
  12. iwa/core/constants.py +28 -0
  13. iwa/core/contracts/__init__.py +1 -0
  14. iwa/core/contracts/contract.py +297 -0
  15. iwa/core/contracts/erc20.py +79 -0
  16. iwa/core/contracts/multisend.py +71 -0
  17. iwa/core/db.py +317 -0
  18. iwa/core/keys.py +361 -0
  19. iwa/core/mnemonic.py +385 -0
  20. iwa/core/models.py +344 -0
  21. iwa/core/monitor.py +209 -0
  22. iwa/core/plugins.py +45 -0
  23. iwa/core/pricing.py +91 -0
  24. iwa/core/services/__init__.py +17 -0
  25. iwa/core/services/account.py +57 -0
  26. iwa/core/services/balance.py +113 -0
  27. iwa/core/services/plugin.py +88 -0
  28. iwa/core/services/safe.py +392 -0
  29. iwa/core/services/transaction.py +172 -0
  30. iwa/core/services/transfer/__init__.py +166 -0
  31. iwa/core/services/transfer/base.py +260 -0
  32. iwa/core/services/transfer/erc20.py +247 -0
  33. iwa/core/services/transfer/multisend.py +386 -0
  34. iwa/core/services/transfer/native.py +262 -0
  35. iwa/core/services/transfer/swap.py +326 -0
  36. iwa/core/settings.py +95 -0
  37. iwa/core/tables.py +60 -0
  38. iwa/core/test.py +27 -0
  39. iwa/core/tests/test_wallet.py +255 -0
  40. iwa/core/types.py +59 -0
  41. iwa/core/ui.py +99 -0
  42. iwa/core/utils.py +59 -0
  43. iwa/core/wallet.py +380 -0
  44. iwa/plugins/__init__.py +1 -0
  45. iwa/plugins/gnosis/__init__.py +5 -0
  46. iwa/plugins/gnosis/cow/__init__.py +6 -0
  47. iwa/plugins/gnosis/cow/quotes.py +148 -0
  48. iwa/plugins/gnosis/cow/swap.py +403 -0
  49. iwa/plugins/gnosis/cow/types.py +20 -0
  50. iwa/plugins/gnosis/cow_utils.py +44 -0
  51. iwa/plugins/gnosis/plugin.py +68 -0
  52. iwa/plugins/gnosis/safe.py +157 -0
  53. iwa/plugins/gnosis/tests/test_cow.py +227 -0
  54. iwa/plugins/gnosis/tests/test_safe.py +100 -0
  55. iwa/plugins/olas/__init__.py +5 -0
  56. iwa/plugins/olas/constants.py +106 -0
  57. iwa/plugins/olas/contracts/activity_checker.py +93 -0
  58. iwa/plugins/olas/contracts/base.py +10 -0
  59. iwa/plugins/olas/contracts/mech.py +49 -0
  60. iwa/plugins/olas/contracts/mech_marketplace.py +43 -0
  61. iwa/plugins/olas/contracts/service.py +215 -0
  62. iwa/plugins/olas/contracts/staking.py +403 -0
  63. iwa/plugins/olas/importer.py +736 -0
  64. iwa/plugins/olas/mech_reference.py +135 -0
  65. iwa/plugins/olas/models.py +110 -0
  66. iwa/plugins/olas/plugin.py +243 -0
  67. iwa/plugins/olas/scripts/test_full_mech_flow.py +259 -0
  68. iwa/plugins/olas/scripts/test_simple_lifecycle.py +74 -0
  69. iwa/plugins/olas/service_manager/__init__.py +60 -0
  70. iwa/plugins/olas/service_manager/base.py +113 -0
  71. iwa/plugins/olas/service_manager/drain.py +336 -0
  72. iwa/plugins/olas/service_manager/lifecycle.py +839 -0
  73. iwa/plugins/olas/service_manager/mech.py +322 -0
  74. iwa/plugins/olas/service_manager/staking.py +530 -0
  75. iwa/plugins/olas/tests/conftest.py +30 -0
  76. iwa/plugins/olas/tests/test_importer.py +128 -0
  77. iwa/plugins/olas/tests/test_importer_error_handling.py +349 -0
  78. iwa/plugins/olas/tests/test_mech_contracts.py +85 -0
  79. iwa/plugins/olas/tests/test_olas_contracts.py +249 -0
  80. iwa/plugins/olas/tests/test_olas_integration.py +561 -0
  81. iwa/plugins/olas/tests/test_olas_models.py +144 -0
  82. iwa/plugins/olas/tests/test_olas_view.py +258 -0
  83. iwa/plugins/olas/tests/test_olas_view_actions.py +137 -0
  84. iwa/plugins/olas/tests/test_olas_view_modals.py +120 -0
  85. iwa/plugins/olas/tests/test_plugin.py +70 -0
  86. iwa/plugins/olas/tests/test_plugin_full.py +212 -0
  87. iwa/plugins/olas/tests/test_service_lifecycle.py +150 -0
  88. iwa/plugins/olas/tests/test_service_manager.py +1065 -0
  89. iwa/plugins/olas/tests/test_service_manager_errors.py +208 -0
  90. iwa/plugins/olas/tests/test_service_manager_flows.py +497 -0
  91. iwa/plugins/olas/tests/test_service_manager_mech.py +135 -0
  92. iwa/plugins/olas/tests/test_service_manager_rewards.py +360 -0
  93. iwa/plugins/olas/tests/test_service_manager_validation.py +145 -0
  94. iwa/plugins/olas/tests/test_service_staking.py +342 -0
  95. iwa/plugins/olas/tests/test_staking_integration.py +269 -0
  96. iwa/plugins/olas/tests/test_staking_validation.py +109 -0
  97. iwa/plugins/olas/tui/__init__.py +1 -0
  98. iwa/plugins/olas/tui/olas_view.py +952 -0
  99. iwa/tools/check_profile.py +67 -0
  100. iwa/tools/release.py +111 -0
  101. iwa/tools/reset_env.py +111 -0
  102. iwa/tools/reset_tenderly.py +362 -0
  103. iwa/tools/restore_backup.py +82 -0
  104. iwa/tui/__init__.py +1 -0
  105. iwa/tui/app.py +174 -0
  106. iwa/tui/modals/__init__.py +5 -0
  107. iwa/tui/modals/base.py +406 -0
  108. iwa/tui/rpc.py +63 -0
  109. iwa/tui/screens/__init__.py +1 -0
  110. iwa/tui/screens/wallets.py +749 -0
  111. iwa/tui/tests/test_app.py +125 -0
  112. iwa/tui/tests/test_rpc.py +139 -0
  113. iwa/tui/tests/test_wallets_refactor.py +30 -0
  114. iwa/tui/tests/test_widgets.py +123 -0
  115. iwa/tui/widgets/__init__.py +5 -0
  116. iwa/tui/widgets/base.py +100 -0
  117. iwa/tui/workers.py +42 -0
  118. iwa/web/dependencies.py +76 -0
  119. iwa/web/models.py +76 -0
  120. iwa/web/routers/accounts.py +115 -0
  121. iwa/web/routers/olas/__init__.py +24 -0
  122. iwa/web/routers/olas/admin.py +169 -0
  123. iwa/web/routers/olas/funding.py +135 -0
  124. iwa/web/routers/olas/general.py +29 -0
  125. iwa/web/routers/olas/services.py +378 -0
  126. iwa/web/routers/olas/staking.py +341 -0
  127. iwa/web/routers/state.py +65 -0
  128. iwa/web/routers/swap.py +617 -0
  129. iwa/web/routers/transactions.py +153 -0
  130. iwa/web/server.py +155 -0
  131. iwa/web/tests/test_web_endpoints.py +713 -0
  132. iwa/web/tests/test_web_olas.py +430 -0
  133. iwa/web/tests/test_web_swap.py +103 -0
  134. iwa-0.0.1a2.dist-info/METADATA +234 -0
  135. iwa-0.0.1a2.dist-info/RECORD +186 -0
  136. iwa-0.0.1a2.dist-info/entry_points.txt +2 -0
  137. iwa-0.0.1a2.dist-info/licenses/LICENSE +21 -0
  138. iwa-0.0.1a2.dist-info/top_level.txt +4 -0
  139. tests/legacy_cow.py +248 -0
  140. tests/legacy_safe.py +93 -0
  141. tests/legacy_transaction_retry_logic.py +51 -0
  142. tests/legacy_tui.py +440 -0
  143. tests/legacy_wallets_screen.py +554 -0
  144. tests/legacy_web.py +243 -0
  145. tests/test_account_service.py +120 -0
  146. tests/test_balance_service.py +186 -0
  147. tests/test_chain.py +490 -0
  148. tests/test_chain_interface.py +210 -0
  149. tests/test_cli.py +139 -0
  150. tests/test_contract.py +195 -0
  151. tests/test_db.py +180 -0
  152. tests/test_drain_coverage.py +174 -0
  153. tests/test_erc20.py +95 -0
  154. tests/test_gnosis_plugin.py +111 -0
  155. tests/test_keys.py +449 -0
  156. tests/test_legacy_wallet.py +1285 -0
  157. tests/test_main.py +13 -0
  158. tests/test_mnemonic.py +217 -0
  159. tests/test_modals.py +109 -0
  160. tests/test_models.py +213 -0
  161. tests/test_monitor.py +202 -0
  162. tests/test_multisend.py +84 -0
  163. tests/test_plugin_service.py +119 -0
  164. tests/test_pricing.py +143 -0
  165. tests/test_rate_limiter.py +199 -0
  166. tests/test_reset_tenderly.py +202 -0
  167. tests/test_rpc_view.py +73 -0
  168. tests/test_safe_coverage.py +139 -0
  169. tests/test_safe_service.py +168 -0
  170. tests/test_service_manager_integration.py +61 -0
  171. tests/test_service_manager_structure.py +31 -0
  172. tests/test_service_transaction.py +176 -0
  173. tests/test_staking_router.py +71 -0
  174. tests/test_staking_simple.py +31 -0
  175. tests/test_tables.py +76 -0
  176. tests/test_transaction_service.py +161 -0
  177. tests/test_transfer_multisend.py +179 -0
  178. tests/test_transfer_native.py +220 -0
  179. tests/test_transfer_security.py +93 -0
  180. tests/test_transfer_structure.py +37 -0
  181. tests/test_transfer_swap_unit.py +155 -0
  182. tests/test_ui_coverage.py +66 -0
  183. tests/test_utils.py +53 -0
  184. tests/test_workers.py +91 -0
  185. tools/verify_drain.py +183 -0
  186. __init__.py +0 -2
  187. hello.py +0 -6
  188. iwa-0.0.0.dist-info/METADATA +0 -10
  189. iwa-0.0.0.dist-info/RECORD +0 -6
  190. iwa-0.0.0.dist-info/top_level.txt +0 -2
  191. {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)}