191 lines
5.8 KiB
Python
191 lines
5.8 KiB
Python
"""
|
|
Input Validation Middleware
|
|
===========================
|
|
|
|
Comprehensive Pydantic-based validation for security endpoints.
|
|
Includes:
|
|
- Request sanitization
|
|
- Type coercion
|
|
- Length/range validation
|
|
- Regex pattern validation
|
|
- Custom validators
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
|
|
from pydantic import BaseModel, Field, validator
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ─── COMMON VALIDATION CONSTANTS ─────────────────────────────────
|
|
|
|
ADDRESS_PATTERN = re.compile(r"^(0x)?[0-9a-fA-F]{38,64}$")
|
|
HASH_PATTERN = re.compile(r"^(0x)?[0-9a-fA-F]{64}$")
|
|
WALLET_PATTERN = re.compile(r"^(0x)?[0-9a-fA-F]{38,64}$")
|
|
CHAIN_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]+$")
|
|
|
|
MIN_ADDRESS_LENGTH = 38 # 0x + 38 hex chars = 20 bytes (EVM)
|
|
MAX_ADDRESS_LENGTH = 66 # 0x + 66 hex chars = 32 bytes (some chains)
|
|
MIN_CHAIN_LENGTH = 2
|
|
MAX_CHAIN_LENGTH = 20
|
|
|
|
# Chain names whitelist
|
|
VALID_CHAINS = {
|
|
"ethereum",
|
|
"base",
|
|
"arbitrum",
|
|
"optimism",
|
|
"polygon",
|
|
"bsc",
|
|
"avalanche",
|
|
"fantom",
|
|
"celo",
|
|
"moonbeam",
|
|
"polygon_zkevm",
|
|
"linea",
|
|
"scroll",
|
|
"zksync",
|
|
"blast",
|
|
"Mode",
|
|
"sepolia",
|
|
"holesky",
|
|
}
|
|
|
|
|
|
# ─── BASE REQUEST MODELS ────────────────────────────────────────
|
|
|
|
|
|
class WalletRequest(BaseModel):
|
|
"""Base model for wallet address requests."""
|
|
|
|
address: str = Field(
|
|
...,
|
|
description="Wallet address (EVM format: 0x...)",
|
|
min_length=MIN_ADDRESS_LENGTH,
|
|
max_length=MAX_ADDRESS_LENGTH,
|
|
)
|
|
chain: str = Field(default="ethereum", description="Blockchain network name")
|
|
|
|
@validator("address")
|
|
def validate_address(self, v: str) -> str:
|
|
"""Validate wallet address format."""
|
|
if not v.startswith("0x"):
|
|
v = "0x" + v
|
|
if not ADDRESS_PATTERN.match(v):
|
|
raise ValueError(f"Invalid address format: {v}")
|
|
return v.lower()
|
|
|
|
@validator("chain")
|
|
def validate_chain(self, v: str) -> str:
|
|
"""Validate chain name."""
|
|
if v not in VALID_CHAINS:
|
|
logger.warning(f"Unknown chain '{v}', allowing anyway for flexibility")
|
|
return v.lower()
|
|
|
|
|
|
class BatchWalletRequest(BaseModel):
|
|
"""Request for batch wallet operations."""
|
|
|
|
addresses: list[str] = Field(..., min_length=1, max_length=50, description="List of wallet addresses")
|
|
chain: str = Field(default="ethereum", description="Blockchain network")
|
|
|
|
@validator("addresses", each_item=True)
|
|
def validate_address(self, v: str) -> str:
|
|
"""Validate each address in batch."""
|
|
if not v.startswith("0x"):
|
|
v = "0x" + v
|
|
if not ADDRESS_PATTERN.match(v):
|
|
raise ValueError(f"Invalid address format: {v}")
|
|
return v.lower()
|
|
|
|
@validator("chain")
|
|
def validate_chain(self, v: str) -> str:
|
|
"""Validate chain name."""
|
|
if v not in VALID_CHAINS:
|
|
logger.warning(f"Unknown chain '{v}', allowing anyway")
|
|
return v.lower()
|
|
|
|
|
|
class ContractRequest(WalletRequest):
|
|
"""Request for contract operations."""
|
|
|
|
deep: bool = Field(default=False, description="Run deep scan with Slither/Mythril")
|
|
|
|
|
|
class TextSearchRequest(BaseModel):
|
|
"""Request for text-based searches."""
|
|
|
|
query: str = Field(..., min_length=1, max_length=200, description="Search query")
|
|
limit: int = Field(default=10, ge=1, le=100, description="Maximum results to return")
|
|
|
|
|
|
# ─── SECURITY-FOCUSED MODELS ────────────────────────────────────
|
|
|
|
|
|
class ThreatCheckRequest(WalletRequest):
|
|
"""Request for threat intelligence checks."""
|
|
|
|
check_sanctions: bool = Field(default=True, description="Check OFAC sanctions list")
|
|
check_risk: bool = Field(default=True, description="Check risk score")
|
|
check_exchange: bool = Field(default=True, description="Check if address is exchange/deposit")
|
|
|
|
|
|
class MempoolMonitorRequest(WalletRequest):
|
|
"""Request for mempool monitoring."""
|
|
|
|
threshold_gas_gwei: float = Field(default=50.0, ge=1.0, le=1000.0, description="Gas price threshold in Gwei")
|
|
alert_on_bot: bool = Field(default=True, description="Alert on bot detection")
|
|
|
|
|
|
class PortfolioRequest(WalletRequest):
|
|
"""Request for portfolio analysis."""
|
|
|
|
include_tokens: bool = Field(default=True, description="Include ERC-20 tokens")
|
|
include_nfts: bool = Field(default=False, description="Include NFTs")
|
|
history_days: int = Field(default=30, ge=1, le=365, description="Days of transaction history")
|
|
|
|
|
|
# ─── UTILITY VALIDATORS ─────────────────────────────────────────
|
|
|
|
|
|
def tier_validator(tier: str, rate_type: str = "requests_per_hour") -> dict[str, int]:
|
|
"""
|
|
Get rate limit for authentication tier.
|
|
|
|
Args:
|
|
tier: Authentication tier (FREE, BASIC, PREMIUM, ENTERPRISE)
|
|
rate_type: Rate type to check
|
|
|
|
Returns:
|
|
Rate limit configuration
|
|
"""
|
|
tiers = {
|
|
"FREE": {"requests_per_hour": 10, "requests_per_day": 100},
|
|
"BASIC": {"requests_per_hour": 50, "requests_per_day": 500},
|
|
"PREMIUM": {"requests_per_hour": 200, "requests_per_day": 2000},
|
|
"ENTERPRISE": {"requests_per_hour": 1000, "requests_per_day": 10000},
|
|
}
|
|
return tiers.get(tier.upper(), tiers["FREE"])
|
|
|
|
|
|
def validate_x402_payment(required: int, tier: str) -> bool:
|
|
"""
|
|
Validate x402 micropayment capability based on tier.
|
|
|
|
Args:
|
|
required: Required x402 tokens
|
|
tier: User's authentication tier
|
|
|
|
Returns:
|
|
True if payment capability is sufficient
|
|
"""
|
|
tier_caps = {
|
|
"FREE": 10,
|
|
"BASIC": 100,
|
|
"PREMIUM": 1000,
|
|
"ENTERPRISE": 10000,
|
|
}
|
|
available = tier_caps.get(tier.upper(), 0)
|
|
return available >= required
|