386 lines
15 KiB
Python
386 lines
15 KiB
Python
"""
|
|
RMI Tools Integration — Wires all installed open-source tools into the backend.
|
|
Tools wired: Ollama, Foundry, Slither, ccxt, web3, blogwatcher, LiteLLM, Vault.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
from datetime import UTC, datetime
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# OLLAMA — Local LLM (port 11434, 2 models)
|
|
# ═══════════════════════════════════════════════════════════
|
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://172.17.0.1:11434")
|
|
|
|
|
|
async def ollama_chat(prompt: str, model: str = "phi3:mini", system: str = "") -> dict:
|
|
"""Use local Ollama for free AI inference."""
|
|
try:
|
|
messages = []
|
|
if system:
|
|
messages.append({"role": "system", "content": system})
|
|
messages.append({"role": "user", "content": prompt})
|
|
async with httpx.AsyncClient(timeout=60) as c:
|
|
r = await c.post(
|
|
f"{OLLAMA_URL}/api/chat",
|
|
json={
|
|
"model": model,
|
|
"messages": messages,
|
|
"stream": False,
|
|
"options": {"temperature": 0.3, "num_predict": 1024},
|
|
},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
return {
|
|
"text": data.get("message", {}).get("content", ""),
|
|
"model": model,
|
|
"provider": "ollama-local",
|
|
"usage": {
|
|
"prompt_tokens": data.get("prompt_eval_count", 0),
|
|
"completion_tokens": data.get("eval_count", 0),
|
|
},
|
|
}
|
|
except Exception as e:
|
|
logger.debug(f"Ollama unavailable: {e}")
|
|
return {"error": "Ollama unavailable"}
|
|
|
|
|
|
async def ollama_list_models() -> list[str]:
|
|
"""Get available Ollama models."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5) as c:
|
|
r = await c.get(f"{OLLAMA_URL}/api/tags")
|
|
if r.status_code == 200:
|
|
return [m["name"] for m in r.json().get("models", [])]
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# FOUNDRY — EVM smart contract analysis (cast)
|
|
# ═══════════════════════════════════════════════════════════
|
|
CAST_PATH = "/root/.foundry/bin/cast"
|
|
|
|
|
|
def cast_contract_info(address: str, chain: str = "ethereum") -> dict:
|
|
"""Get contract bytecode, ABI, and metadata using cast."""
|
|
rpcs = {
|
|
"ethereum": os.getenv("ETH_RPC_URL", "https://eth.llamarpc.com"),
|
|
"base": os.getenv("BASE_RPC_URL", "https://base.llamarpc.com"),
|
|
"arbitrum": os.getenv("ARB_RPC_URL", "https://arb1.arbitrum.io/rpc"),
|
|
"polygon": os.getenv("POLY_RPC_URL", "https://polygon.llamarpc.com"),
|
|
"bsc": os.getenv("BSC_RPC_URL", "https://bsc-dataseed.binance.org"),
|
|
"avalanche": os.getenv("AVAX_RPC_URL", "https://api.avax.network/ext/bc/C/rpc"),
|
|
}
|
|
rpc = rpcs.get(chain, rpcs["ethereum"])
|
|
try:
|
|
env = os.environ.copy()
|
|
env["ETH_RPC_URL"] = rpc
|
|
|
|
# Get bytecode
|
|
code = subprocess.run(
|
|
[CAST_PATH, "code", address, "--rpc-url", rpc],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=15,
|
|
)
|
|
bytecode = code.stdout.strip()
|
|
|
|
# Get nonce
|
|
nonce = subprocess.run(
|
|
[CAST_PATH, "nonce", address, "--rpc-url", rpc],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
|
|
# Get balance
|
|
balance = subprocess.run(
|
|
[CAST_PATH, "balance", address, "--rpc-url", rpc],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
|
|
return {
|
|
"address": address,
|
|
"chain": chain,
|
|
"has_bytecode": len(bytecode) > 4,
|
|
"bytecode_size": len(bytecode) // 2 if bytecode.startswith("0x") else 0,
|
|
"is_contract": len(bytecode) > 4,
|
|
"nonce": nonce.stdout.strip(),
|
|
"balance_wei": balance.stdout.strip(),
|
|
"balanced_checked": datetime.now(UTC).isoformat(),
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"address": address,
|
|
"chain": chain,
|
|
"error": str(e),
|
|
"cast_available": os.path.exists(CAST_PATH),
|
|
}
|
|
|
|
|
|
def cast_tx_decode(tx_hash: str, chain: str = "ethereum") -> dict:
|
|
"""Decode a transaction using cast."""
|
|
rpcs = {
|
|
"ethereum": "https://eth.llamarpc.com",
|
|
"base": "https://base.llamarpc.com",
|
|
"arbitrum": "https://arb1.arbitrum.io/rpc",
|
|
"bsc": "https://bsc-dataseed.binance.org",
|
|
}
|
|
rpc = rpcs.get(chain, rpcs["ethereum"])
|
|
try:
|
|
result = subprocess.run(
|
|
[CAST_PATH, "tx", tx_hash, "--rpc-url", rpc, "--json"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=15,
|
|
)
|
|
if result.returncode == 0:
|
|
return json.loads(result.stdout)
|
|
except Exception:
|
|
pass
|
|
return {"error": "Decode failed", "tx_hash": tx_hash}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# SLITHER — Solidity vulnerability scanner
|
|
# ═══════════════════════════════════════════════════════════
|
|
def slither_analyze(contract_address: str, chain: str = "ethereum") -> dict:
|
|
"""Run Slither on a verified contract."""
|
|
try:
|
|
result = subprocess.run(
|
|
["slither", contract_address, "--print", "human-summary"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
return {
|
|
"address": contract_address,
|
|
"chain": chain,
|
|
"analysis": result.stdout[:2000] if result.returncode == 0 else result.stderr[:500],
|
|
"success": result.returncode == 0,
|
|
}
|
|
except Exception as e:
|
|
return {"error": str(e), "slither_available": False}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# CCXT — 100+ exchange price feeds
|
|
# ═══════════════════════════════════════════════════════════
|
|
def ccxt_get_prices(symbols: list[str] | None = None) -> dict:
|
|
"""Get real-time prices from multiple exchanges via ccxt."""
|
|
try:
|
|
import ccxt
|
|
|
|
exchanges = {
|
|
"binance": ccxt.binance(),
|
|
"kraken": ccxt.kraken(),
|
|
"coinbase": ccxt.coinbase(),
|
|
"bybit": ccxt.bybit(),
|
|
}
|
|
results = {}
|
|
default_symbols = [
|
|
"BTC/USDT",
|
|
"ETH/USDT",
|
|
"SOL/USDT",
|
|
"BNB/USDT",
|
|
"ARB/USDT",
|
|
"MATIC/USDT",
|
|
"AVAX/USDT",
|
|
]
|
|
for sym in symbols or default_symbols:
|
|
for ex_name, ex in exchanges.items():
|
|
try:
|
|
ticker = ex.fetch_ticker(sym)
|
|
if ticker and ticker.get("last"):
|
|
results.setdefault(sym, {})[ex_name] = {
|
|
"price": ticker["last"],
|
|
"change_24h": ticker.get("percentage", ticker.get("change", 0)),
|
|
"volume_24h": ticker.get("quoteVolume", ticker.get("baseVolume", 0)),
|
|
}
|
|
except Exception:
|
|
pass
|
|
return {
|
|
"prices": results,
|
|
"exchanges": list(exchanges.keys()),
|
|
"updated_at": datetime.now(UTC).isoformat(),
|
|
}
|
|
except ImportError:
|
|
return {"error": "ccxt not installed", "prices": {}}
|
|
except Exception as e:
|
|
return {"error": str(e), "prices": {}}
|
|
|
|
|
|
def ccxt_arbitrage(symbol: str = "BTC/USDT") -> dict:
|
|
"""Find arbitrage opportunities across exchanges."""
|
|
prices = ccxt_get_prices([symbol])
|
|
price_data = prices.get("prices", {}).get(symbol, {})
|
|
if len(price_data) >= 2:
|
|
prices_list = [(ex, p["price"]) for ex, p in price_data.items()]
|
|
lowest = min(prices_list, key=lambda x: x[1])
|
|
highest = max(prices_list, key=lambda x: x[1])
|
|
spread = ((highest[1] - lowest[1]) / lowest[1]) * 100
|
|
return {
|
|
"symbol": symbol,
|
|
"lowest": {"exchange": lowest[0], "price": lowest[1]},
|
|
"highest": {"exchange": highest[0], "price": highest[1]},
|
|
"spread_pct": round(spread, 4),
|
|
"profitable": spread > 0.5,
|
|
}
|
|
return {"symbol": symbol, "opportunities": 0}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# WEB3.PY — Direct Ethereum interaction
|
|
# ═══════════════════════════════════════════════════════════
|
|
def web3_wallet_balance(address: str, chain: str = "ethereum") -> dict:
|
|
"""Get wallet balance and token holdings via web3."""
|
|
try:
|
|
from web3 import Web3
|
|
|
|
rpcs = {
|
|
"ethereum": "https://eth.llamarpc.com",
|
|
"base": "https://base.llamarpc.com",
|
|
"bsc": "https://bsc-dataseed.binance.org",
|
|
"polygon": "https://polygon.llamarpc.com",
|
|
"arbitrum": "https://arb1.arbitrum.io/rpc",
|
|
"avalanche": "https://api.avax.network/ext/bc/C/rpc",
|
|
}
|
|
rpc = rpcs.get(chain, rpcs["ethereum"])
|
|
w3 = Web3(Web3.HTTPProvider(rpc))
|
|
|
|
if not w3.is_connected():
|
|
return {"error": f"RPC unavailable for {chain}"}
|
|
|
|
checksum = w3.to_checksum_address(address)
|
|
balance = w3.eth.get_balance(checksum)
|
|
tx_count = w3.eth.get_transaction_count(checksum)
|
|
code = w3.eth.get_code(checksum)
|
|
|
|
return {
|
|
"address": address,
|
|
"chain": chain,
|
|
"balance_eth": round(w3.from_wei(balance, "ether"), 6),
|
|
"balance_wei": balance,
|
|
"transaction_count": tx_count,
|
|
"is_contract": len(code) > 0,
|
|
"rpc": rpc,
|
|
}
|
|
except ImportError:
|
|
return {"error": "web3 not installed"}
|
|
except Exception as e:
|
|
return {"error": str(e), "address": address, "chain": chain}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# BLOGWATCHER — RSS/Atom feed monitoring
|
|
# ═══════════════════════════════════════════════════════════
|
|
BLOGWATCHER_PATH = "/root/go/bin/blogwatcher"
|
|
|
|
|
|
def blogwatcher_fetch(feed_url: str, limit: int = 10) -> dict:
|
|
"""Fetch RSS/Atom feed using blogwatcher."""
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
BLOGWATCHER_PATH,
|
|
"fetch",
|
|
"--url",
|
|
feed_url,
|
|
"--limit",
|
|
str(limit),
|
|
"--output",
|
|
"json",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
if result.returncode == 0:
|
|
posts = json.loads(result.stdout) if result.stdout.strip() else []
|
|
return {"feed": feed_url, "posts": posts, "count": len(posts)}
|
|
except Exception:
|
|
pass
|
|
return {"feed": feed_url, "posts": [], "error": "blogwatcher unavailable"}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# LITELLM — Multi-provider LLM proxy routing
|
|
# ═══════════════════════════════════════════════════════════
|
|
LITELLM_CONFIG = "/srv/rugmuncher-backend/litellm/config.yaml"
|
|
|
|
|
|
def litellm_status() -> dict:
|
|
"""Check LiteLLM proxy availability."""
|
|
config_exists = os.path.exists(LITELLM_CONFIG)
|
|
return {
|
|
"available": config_exists,
|
|
"config_path": LITELLM_CONFIG,
|
|
"config_size": os.path.getsize(LITELLM_CONFIG) if config_exists else 0,
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# VAULT — Secrets management
|
|
# ═══════════════════════════════════════════════════════════
|
|
VAULT_ADDR = os.getenv("VAULT_ADDR", "http://172.17.0.1:8200")
|
|
|
|
|
|
def vault_get_secret(path: str) -> dict | None:
|
|
"""Retrieve a secret from HashiCorp Vault."""
|
|
try:
|
|
r = httpx.get(f"{VAULT_ADDR}/v1/{path}", headers={"X-Vault-Token": "root"}, timeout=5)
|
|
if r.status_code == 200:
|
|
return r.json().get("data", {}).get("data", {})
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def vault_list_secrets(path: str = "secret") -> list[str]:
|
|
"""List secrets in Vault."""
|
|
try:
|
|
r = httpx.get(f"{VAULT_ADDR}/v1/{path}?list=true", headers={"X-Vault-Token": "root"}, timeout=5)
|
|
if r.status_code == 200:
|
|
return r.json().get("data", {}).get("keys", [])
|
|
except Exception:
|
|
pass
|
|
return []
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════
|
|
# TOOL STATUS — Health check for all integrated tools
|
|
# ═══════════════════════════════════════════════════════════
|
|
async def tools_health() -> dict:
|
|
"""Health check for all integrated tools."""
|
|
return {
|
|
"ollama": {
|
|
"available": bool(await ollama_list_models()),
|
|
"models": await ollama_list_models(),
|
|
"url": OLLAMA_URL,
|
|
},
|
|
"foundry": {"installed": os.path.exists(CAST_PATH), "path": CAST_PATH},
|
|
"slither": {"installed": os.path.exists("/root/.local/bin/slither")},
|
|
"ccxt": {
|
|
"available": True # pip installed
|
|
},
|
|
"web3": {"available": True},
|
|
"blogwatcher": {"installed": os.path.exists(BLOGWATCHER_PATH), "path": BLOGWATCHER_PATH},
|
|
"litellm": litellm_status(),
|
|
"vault": {"available": bool(vault_list_secrets()), "url": VAULT_ADDR},
|
|
"file_upload": {
|
|
"running": True, # port 8001
|
|
"url": "http://localhost:8001",
|
|
},
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|