- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
278 lines
10 KiB
Python
278 lines
10 KiB
Python
"""
|
|
Transaction Simulation - Pre-flight swap/sell simulation.
|
|
|
|
Detects honeypots, excessive taxes, and reverts BEFORE the user signs.
|
|
Simulates a sell transaction using Jupiter (Solana) and eth_call (EVM) to
|
|
predict what will happen without spending gas.
|
|
|
|
Architecture:
|
|
- Solana: Jupiter quote API → simulate via Helius RPC
|
|
- EVM: eth_call with swap parameters
|
|
- Returns: success/fail, expected output, tax rate, risk assessment
|
|
"""
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class SimulationResult:
|
|
"""Result of a transaction simulation."""
|
|
|
|
token_address: str
|
|
chain: str
|
|
can_sell: bool
|
|
can_buy: bool
|
|
sell_tax_pct: float = 0.0
|
|
buy_tax_pct: float = 0.0
|
|
expected_output: float | None = None
|
|
expected_output_token: str = ""
|
|
simulation_success: bool = False
|
|
error: str | None = None
|
|
warnings: list[str] = field(default_factory=list)
|
|
risk: str = "unknown" # safe, warning, dangerous, critical
|
|
|
|
@property
|
|
def is_honeypot(self) -> bool:
|
|
return not self.can_sell and self.can_buy
|
|
|
|
@property
|
|
def is_hard_rug(self) -> bool:
|
|
return not self.can_sell and not self.can_buy
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# SOLANA SIMULATION (via Jupiter + Helius)
|
|
# ═══════════════════════════════════════════════
|
|
|
|
JUPITER_QUOTE = "https://quote-api.jup.ag/v6/quote"
|
|
JUPITER_SWAP = "https://quote-api.jup.ag/v6/swap"
|
|
WSOL_MINT = "So11111111111111111111111111111111111111112"
|
|
SIMULATION_AMOUNT = 100_000 # 0.0001 SOL for simulation
|
|
|
|
|
|
async def simulate_solana(token_address: str) -> SimulationResult:
|
|
"""Simulate a sell transaction on Solana."""
|
|
result = SimulationResult(
|
|
token_address=token_address,
|
|
chain="solana",
|
|
can_sell=False,
|
|
can_buy=False,
|
|
)
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
# Step 1: Try to sell (token → SOL)
|
|
sell_quote = await _get_quote(client, token_address, WSOL_MINT, SIMULATION_AMOUNT)
|
|
|
|
if sell_quote:
|
|
result.can_sell = True
|
|
result.expected_output = float(sell_quote.get("outAmount", 0)) / 1e9
|
|
result.expected_output_token = "SOL"
|
|
|
|
# Calculate tax from price impact + slippage
|
|
price_impact = float(sell_quote.get("priceImpactPct", 0))
|
|
if price_impact > 50:
|
|
result.warnings.append(f"Very high price impact: {price_impact:.1f}%")
|
|
result.sell_tax_pct = price_impact
|
|
elif price_impact > 15:
|
|
result.warnings.append(f"High price impact: {price_impact:.1f}%")
|
|
result.sell_tax_pct = price_impact
|
|
|
|
# Step 2: Try to buy (SOL → token)
|
|
buy_quote = await _get_quote(client, WSOL_MINT, token_address, SIMULATION_AMOUNT)
|
|
if buy_quote:
|
|
result.can_buy = True
|
|
price_impact = float(buy_quote.get("priceImpactPct", 0))
|
|
if price_impact > 50:
|
|
result.buy_tax_pct = price_impact
|
|
|
|
result.simulation_success = True
|
|
|
|
# Risk assessment
|
|
if result.is_honeypot:
|
|
result.risk = "critical"
|
|
result.warnings.append("⚠️ HONEYPOT DETECTED: Can buy but cannot sell!")
|
|
elif not result.can_sell:
|
|
result.risk = "critical"
|
|
result.warnings.append("⚠️ Cannot sell this token - liquidity may not exist")
|
|
elif result.sell_tax_pct > 90:
|
|
result.risk = "critical"
|
|
result.warnings.append(f"☠️ {result.sell_tax_pct:.0f}% effective tax on sell - likely scam")
|
|
elif result.sell_tax_pct > 50:
|
|
result.risk = "dangerous"
|
|
result.warnings.append(f"🔴 {result.sell_tax_pct:.0f}% tax on sell")
|
|
elif result.sell_tax_pct > 15:
|
|
result.risk = "warning"
|
|
result.warnings.append(f"🟠 {result.sell_tax_pct:.0f}% price impact on sell")
|
|
elif result.can_sell and result.can_buy:
|
|
result.risk = "safe"
|
|
|
|
except Exception as e:
|
|
result.error = str(e)
|
|
result.risk = "unknown"
|
|
result.warnings.append(f"Simulation failed: {e}")
|
|
|
|
return result
|
|
|
|
|
|
async def _get_quote(client: httpx.AsyncClient, input_mint: str, output_mint: str, amount: int) -> dict | None:
|
|
"""Get a Jupiter swap quote."""
|
|
try:
|
|
params = {
|
|
"inputMint": input_mint,
|
|
"outputMint": output_mint,
|
|
"amount": str(amount),
|
|
"slippageBps": 100, # 1% slippage
|
|
}
|
|
r = await client.get(JUPITER_QUOTE, params=params)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
if data.get("outAmount"):
|
|
return data
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# EVM SIMULATION (via eth_call)
|
|
# ═══════════════════════════════════════════════
|
|
|
|
UNISWAP_V2_ROUTER = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
|
|
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
|
|
|
# Common public RPC endpoints
|
|
EVM_RPC_ENDPOINTS = {
|
|
"ethereum": "https://eth.llamarpc.com",
|
|
"bsc": "https://bsc-dataseed.binance.org",
|
|
"polygon": "https://polygon.llamarpc.com",
|
|
"arbitrum": "https://arb1.arbitrum.io/rpc",
|
|
"base": "https://mainnet.base.org",
|
|
"optimism": "https://mainnet.optimism.io",
|
|
}
|
|
|
|
|
|
async def simulate_evm(token_address: str, chain: str = "ethereum") -> SimulationResult:
|
|
"""Simulate a sell on EVM chains via eth_call."""
|
|
result = SimulationResult(
|
|
token_address=token_address,
|
|
chain=chain,
|
|
can_sell=False,
|
|
can_buy=False,
|
|
)
|
|
|
|
rpc_url = EVM_RPC_ENDPOINTS.get(chain, EVM_RPC_ENDPOINTS["ethereum"])
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
# Check if token is accessible via eth_call
|
|
# Call balanceOf to verify token exists
|
|
balance_check = await _eth_call(
|
|
client,
|
|
rpc_url,
|
|
token_address,
|
|
"0x70a08231000000000000000000000000" + "0" * 40, # balanceOf(address(0))
|
|
)
|
|
if balance_check and not balance_check.get("error"):
|
|
result.can_buy = True # Token exists and is callable
|
|
|
|
# Try to simulate a sell via Uniswap
|
|
sell_result = await _simulate_uniswap_sell(client, rpc_url, token_address)
|
|
if sell_result.get("success"):
|
|
result.can_sell = True
|
|
result.expected_output = sell_result.get("output_amount")
|
|
result.expected_output_token = "ETH"
|
|
|
|
result.simulation_success = True
|
|
|
|
if result.is_honeypot:
|
|
result.risk = "critical"
|
|
result.warnings.append("⚠️ HONEYPOT: Token exists but sells revert")
|
|
elif result.can_sell:
|
|
result.risk = "safe"
|
|
else:
|
|
result.risk = "warning"
|
|
result.warnings.append("Could not verify sell path - exercise caution")
|
|
|
|
except Exception as e:
|
|
result.error = str(e)
|
|
result.risk = "unknown"
|
|
|
|
return result
|
|
|
|
|
|
async def _eth_call(client: httpx.AsyncClient, rpc_url: str, to: str, data: str) -> dict:
|
|
"""Execute an eth_call JSON-RPC request."""
|
|
try:
|
|
r = await client.post(
|
|
rpc_url,
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "eth_call",
|
|
"params": [{"to": to, "data": data}, "latest"],
|
|
},
|
|
)
|
|
return r.json()
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
async def _simulate_uniswap_sell(client: httpx.AsyncClient, rpc_url: str, token: str) -> dict:
|
|
"""Simulate a Uniswap V2 sell transaction."""
|
|
try:
|
|
# Encode getAmountsOut(amountIn, [token, WETH])
|
|
amount_in_hex = "0x" + format(1000000000000000000, "064x") # 1 ETH worth
|
|
data = (
|
|
"0xd06ca61f"
|
|
+ amount_in_hex[2:].zfill(64)
|
|
+ "0000000000000000000000000000000000000000000000000000000000000040"
|
|
+ "0000000000000000000000000000000000000000000000000000000000000002"
|
|
+ "000000000000000000000000"
|
|
+ token[2:].lower().zfill(64)
|
|
+ "000000000000000000000000"
|
|
+ WETH[2:].lower().zfill(64)
|
|
)
|
|
r = await client.post(
|
|
rpc_url,
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "eth_call",
|
|
"params": [{"to": UNISWAP_V2_ROUTER, "data": data}, "latest"],
|
|
},
|
|
)
|
|
result = r.json()
|
|
if "result" in result and result["result"] != "0x":
|
|
# Parse output amount from ABI-encoded response
|
|
raw = result["result"]
|
|
return {"success": True, "output_amount": int(raw, 16) / 1e18}
|
|
return {"success": False}
|
|
except Exception:
|
|
return {"success": False}
|
|
|
|
|
|
# ═══════════════════════════════════════════════
|
|
# UNIFIED SIMULATION
|
|
# ═══════════════════════════════════════════════
|
|
|
|
|
|
async def simulate_transaction(token_address: str, chain: str = "solana") -> SimulationResult:
|
|
"""
|
|
Simulate a sell transaction for any chain.
|
|
|
|
Returns a SimulationResult with:
|
|
- can_sell / can_buy (honeypot detection)
|
|
- estimated tax/price impact
|
|
- risk assessment
|
|
- warnings
|
|
"""
|
|
if chain == "solana" or (len(token_address) in range(32, 45) and not token_address.startswith("0x")):
|
|
return await simulate_solana(token_address)
|
|
else:
|
|
return await simulate_evm(token_address, chain)
|