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
@@ -0,0 +1,153 @@
1
+ """Transactions Router for Web API."""
2
+
3
+ import datetime
4
+ import json
5
+ import logging
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException, Request
8
+ from pydantic import BaseModel, Field, field_validator
9
+ from slowapi import Limiter
10
+ from slowapi.util import get_remote_address
11
+ from web3 import Web3
12
+
13
+ from iwa.core.db import SentTransaction
14
+ from iwa.web.dependencies import verify_auth, wallet
15
+
16
+ logger = logging.getLogger(__name__)
17
+ router = APIRouter(prefix="/api", tags=["transactions"])
18
+
19
+ # Rate limiter for this router
20
+ limiter = Limiter(key_func=get_remote_address)
21
+
22
+
23
+ class TransactionRequest(BaseModel):
24
+ """Request model for sending a transaction."""
25
+
26
+ from_address: str = Field(description="Sender address or tag")
27
+ to_address: str = Field(description="Recipient address or tag")
28
+ amount_eth: float = Field(description="Amount to send in ETH/Tokens")
29
+ token: str = Field(default="native", description="Token symbol (e.g., OLAS) or 'native'")
30
+ chain: str = Field(default="gnosis", description="Target blockchain")
31
+
32
+ @field_validator("from_address", "to_address")
33
+ @classmethod
34
+ def validate_address(cls, v: str) -> str:
35
+ """Validate address format."""
36
+ if not v:
37
+ raise ValueError("Address cannot be empty")
38
+ if v.startswith("0x"):
39
+ if len(v) != 42:
40
+ raise ValueError("Invalid address format")
41
+ else:
42
+ # Assume it's a tag - allow alphanumeric, underscores, dashes, and spaces
43
+ if not v.replace("_", "").replace("-", "").replace(" ", "").isalnum():
44
+ raise ValueError("Invalid tag format")
45
+ return v
46
+
47
+ @field_validator("chain")
48
+ @classmethod
49
+ def validate_chain(cls, v: str) -> str:
50
+ """Validate chain name."""
51
+ if not v.replace("-", "").isalnum():
52
+ raise ValueError("Invalid chain name")
53
+ return v
54
+
55
+ @field_validator("token")
56
+ @classmethod
57
+ def validate_token(cls, v: str) -> str:
58
+ """Validate token symbol or address."""
59
+ # Token can be "native", a symbol "OLAS", or address "0x..."
60
+ if not v:
61
+ raise ValueError("Token cannot be empty")
62
+ if v.startswith("0x") and len(v) != 42:
63
+ raise ValueError("Invalid token address")
64
+ return v
65
+
66
+ @field_validator("amount_eth")
67
+ @classmethod
68
+ def validate_amount(cls, v: float) -> float:
69
+ """Validate amount is positive."""
70
+ if v < 0:
71
+ raise ValueError("Amount must be positive")
72
+ if v > 1e18: # Sanity check
73
+ raise ValueError("Amount too large")
74
+ return v
75
+
76
+
77
+ @router.get(
78
+ "/transactions",
79
+ summary="Get Transactions",
80
+ description="Retrieve recent sent transactions (last 24h) for a chain.",
81
+ )
82
+ def get_transactions(chain: str = "gnosis", auth: bool = Depends(verify_auth)):
83
+ """Get recent transactions for a specific chain."""
84
+ if not chain.replace("-", "").isalnum():
85
+ raise HTTPException(status_code=400, detail="Invalid chain name")
86
+ chain = chain.lower()
87
+ recent = (
88
+ SentTransaction.select()
89
+ .where(
90
+ (SentTransaction.chain == chain)
91
+ & (SentTransaction.timestamp > (datetime.datetime.now() - datetime.timedelta(hours=24)))
92
+ )
93
+ .order_by(SentTransaction.timestamp.desc())
94
+ )
95
+
96
+ result = []
97
+ for tx in recent:
98
+ # Get token decimals for proper display
99
+ token_decimals = 18 # Default for native
100
+ if tx.token and tx.token.lower() not in ["native", "native currency"]:
101
+ try:
102
+ from iwa.core.chain import ChainInterfaces
103
+ from iwa.core.contracts.erc20 import ERC20Contract
104
+
105
+ chain_interface = ChainInterfaces().get(chain)
106
+ if chain_interface:
107
+ token_address = chain_interface.chain.get_token_address(tx.token)
108
+ if token_address:
109
+ erc20 = ERC20Contract(token_address, chain)
110
+ token_decimals = erc20.decimals
111
+ except Exception:
112
+ pass # Default to 18 if we can't get decimals
113
+
114
+ amount_display = float(tx.amount_wei or 0) / (10**token_decimals)
115
+
116
+ result.append(
117
+ {
118
+ "timestamp": tx.timestamp.isoformat(),
119
+ "chain": tx.chain.capitalize(),
120
+ "from": tx.from_tag or tx.from_address,
121
+ "to": tx.to_tag or tx.to_address,
122
+ "token": tx.token,
123
+ "amount": f"{amount_display:.2f}",
124
+ "value_eur": f"€{(tx.value_eur or 0.0):.2f}",
125
+ "status": "Confirmed",
126
+ "hash": tx.tx_hash,
127
+ "gas_cost": str(tx.gas_cost or "0"),
128
+ "gas_value_eur": f"€{tx.gas_value_eur:.4f}" if tx.gas_value_eur else "?",
129
+ "tags": json.loads(tx.tags) if tx.tags else [],
130
+ }
131
+ )
132
+ return result
133
+
134
+
135
+ @router.post(
136
+ "/send",
137
+ summary="Send Transaction",
138
+ description="Send native currency or ERC20 tokens from a managed account.",
139
+ )
140
+ @limiter.limit("10/minute")
141
+ def send_transaction(request: Request, req: TransactionRequest, auth: bool = Depends(verify_auth)):
142
+ """Send a transaction from an account."""
143
+ try:
144
+ tx_hash = wallet.send(
145
+ from_address_or_tag=req.from_address,
146
+ to_address_or_tag=req.to_address,
147
+ amount_wei=Web3.to_wei(req.amount_eth, "ether"),
148
+ token_address_or_name=req.token,
149
+ chain_name=req.chain,
150
+ )
151
+ return {"status": "success", "hash": tx_hash}
152
+ except Exception as e:
153
+ raise HTTPException(status_code=400, detail=str(e)) from None
iwa/web/server.py ADDED
@@ -0,0 +1,155 @@
1
+ """FastAPI Server Entrypoint."""
2
+
3
+ import logging
4
+ import os
5
+ from contextlib import asynccontextmanager
6
+ from pathlib import Path
7
+
8
+ from fastapi import FastAPI, Request
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import HTMLResponse, JSONResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from slowapi import Limiter, _rate_limit_exceeded_handler
13
+ from slowapi.errors import RateLimitExceeded
14
+ from slowapi.util import get_remote_address
15
+ from starlette.middleware.base import BaseHTTPMiddleware
16
+
17
+ from iwa.core.wallet import init_db
18
+
19
+ # Pre-load cowdao_cowpy modules BEFORE async loop starts
20
+ # This is required because cowdao_cowpy uses asyncio.run() at import time
21
+ # which fails if called from an already running event loop
22
+ from iwa.plugins.gnosis.cow_utils import get_cowpy_module
23
+
24
+ get_cowpy_module("DEFAULT_APP_DATA_HASH") # Forces import now, not during async
25
+
26
+ # Import dependencies to ensure initialization
27
+ # Import routers
28
+ from iwa.web.routers import accounts, olas, state, swap, transactions # noqa: E402
29
+
30
+ # Configure logging
31
+ logging.basicConfig(level=logging.INFO)
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ # Rate limiter (in-memory storage, resets on restart)
36
+ limiter = Limiter(key_func=get_remote_address)
37
+
38
+
39
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
40
+ """Middleware to add security headers to all responses."""
41
+
42
+ async def dispatch(self, request: Request, call_next):
43
+ """Add security headers to response."""
44
+ response = await call_next(request)
45
+ response.headers["X-Content-Type-Options"] = "nosniff"
46
+ response.headers["X-Frame-Options"] = "DENY"
47
+ response.headers["X-XSS-Protection"] = "1; mode=block"
48
+ response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
49
+ response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
50
+ # Content Security Policy for XSS protection
51
+ response.headers["Content-Security-Policy"] = (
52
+ "default-src 'self'; "
53
+ "script-src 'self'; "
54
+ "style-src 'self'; "
55
+ "img-src 'self' data:; "
56
+ "font-src 'self'; "
57
+ "connect-src 'self'"
58
+ )
59
+ # HSTS for production HTTPS deployments (enable via environment variable)
60
+ if os.getenv("ENABLE_HSTS", "").lower() in ("true", "1", "yes"):
61
+ response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
62
+ return response
63
+
64
+
65
+ @asynccontextmanager
66
+ async def lifespan(app: FastAPI):
67
+ """Lifecycle events."""
68
+ logger.info("Starting up check operations...")
69
+ init_db()
70
+
71
+ # Initialize block tracking for Tenderly monitoring
72
+ from iwa.core.chain import ChainInterfaces
73
+
74
+ ChainInterfaces().gnosis.init_block_tracking()
75
+ # Check block limit immediately at startup with visual progress bar
76
+ ChainInterfaces().gnosis.check_block_limit(show_progress_bar=True)
77
+
78
+ yield
79
+ logger.info("Shutting down...")
80
+
81
+
82
+ app = FastAPI(title="IWA Web UI", version="0.1.0", lifespan=lifespan)
83
+
84
+ # Attach rate limiter to app
85
+ app.state.limiter = limiter
86
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
87
+
88
+ # Security Headers Middleware
89
+ app.add_middleware(SecurityHeadersMiddleware)
90
+
91
+ # CORS - configurable via environment variable for production
92
+ default_origins = [
93
+ "http://localhost:3000",
94
+ "http://127.0.0.1:3000",
95
+ "http://localhost:8080",
96
+ "http://127.0.0.1:8080",
97
+ ]
98
+ allowed_origins_env = os.getenv("ALLOWED_ORIGINS")
99
+ if allowed_origins_env:
100
+ origins = [origin.strip() for origin in allowed_origins_env.split(",")]
101
+ else:
102
+ origins = default_origins
103
+
104
+ app.add_middleware(
105
+ CORSMiddleware,
106
+ allow_origins=origins,
107
+ allow_credentials=True,
108
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
109
+ allow_headers=["*"],
110
+ )
111
+
112
+
113
+ # Exception Handler
114
+ @app.exception_handler(Exception)
115
+ async def global_exception_handler(request: Request, exc: Exception):
116
+ """Global exception handler for the API."""
117
+ logger.error(f"Global exception: {exc}", exc_info=True)
118
+ return JSONResponse(
119
+ status_code=500,
120
+ content={"detail": "Internal Server Error. Check logs for details."},
121
+ )
122
+
123
+
124
+ # Include Routers
125
+ app.include_router(state.router)
126
+ app.include_router(accounts.router)
127
+ app.include_router(transactions.router)
128
+ app.include_router(swap.router)
129
+ app.include_router(olas.router)
130
+
131
+ # Mount Static Files at /static/ path
132
+ static_dir = Path(__file__).parent / "static"
133
+ if static_dir.exists():
134
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
135
+
136
+
137
+ # Serve index.html for root path
138
+ @app.get("/", response_class=HTMLResponse)
139
+ async def root():
140
+ """Serve the main HTML page."""
141
+ index_path = static_dir / "index.html"
142
+ if index_path.exists():
143
+ return index_path.read_text()
144
+ return HTMLResponse(content="<h1>IWA Web UI</h1><p>index.html not found</p>", status_code=200)
145
+
146
+
147
+ def run_server(host: str = "127.0.0.1", port: int = 8000):
148
+ """Run the web server using uvicorn."""
149
+ import uvicorn
150
+
151
+ uvicorn.run(app, host=host, port=port)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ run_server()