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
|
@@ -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()
|