Automated Action cae8527a4b s
2025-05-28 22:44:22 +00:00

218 lines
8.0 KiB
Python

import json
import logging
from typing import Dict, List, Optional, Tuple
from solana.rpc.api import Client
from solana.rpc.types import MemcmpOpts
from app.core.config import settings
logger = logging.getLogger(__name__)
class SolanaClient:
"""
Client for interacting with Solana blockchain
"""
def __init__(self, rpc_url: str = None):
"""
Initialize Solana client with RPC URL
"""
self.rpc_url = rpc_url or settings.SOLANA_RPC_URL
self.client = Client(self.rpc_url)
def get_latest_block(self) -> Dict:
"""
Get latest finalized block
"""
response = self.client.get_latest_blockhash()
if "error" in response:
logger.error(f"Error getting latest block: {response['error']}")
raise Exception(f"Error getting latest block: {response['error']}")
return response["result"]
def get_block(self, slot: int) -> Dict:
"""
Get block data by slot number
"""
response = self.client.get_block(
slot,
encoding="json",
max_supported_transaction_version=0,
transaction_details="full",
)
if "error" in response:
logger.error(f"Error getting block {slot}: {response['error']}")
raise Exception(f"Error getting block {slot}: {response['error']}")
return response["result"]
def get_blocks(self, start_slot: int, end_slot: int = None) -> List[int]:
"""
Get a list of confirmed blocks
"""
if end_slot is None:
# Get latest slot if end_slot not provided
response = self.client.get_slot()
if "error" in response:
logger.error(f"Error getting latest slot: {response['error']}")
raise Exception(f"Error getting latest slot: {response['error']}")
end_slot = response["result"]
response = self.client.get_blocks(start_slot, end_slot)
if "error" in response:
logger.error(f"Error getting blocks from {start_slot} to {end_slot}: {response['error']}")
raise Exception(f"Error getting blocks from {start_slot} to {end_slot}: {response['error']}")
return response["result"]
def get_transaction(self, signature: str) -> Dict:
"""
Get transaction details by signature
"""
response = self.client.get_transaction(
signature,
encoding="json",
max_supported_transaction_version=0,
)
if "error" in response:
logger.error(f"Error getting transaction {signature}: {response['error']}")
raise Exception(f"Error getting transaction {signature}: {response['error']}")
return response["result"]
def get_token_accounts_by_owner(self, owner: str, program_id: str = None) -> List[Dict]:
"""
Get token accounts by owner
"""
filters = []
if program_id:
filters.append(
MemcmpOpts(
offset=0,
bytes=program_id,
)
)
response = self.client.get_token_accounts_by_owner(
owner,
filters,
encoding="jsonParsed",
)
if "error" in response:
logger.error(f"Error getting token accounts for {owner}: {response['error']}")
raise Exception(f"Error getting token accounts for {owner}: {response['error']}")
return response["result"]["value"]
def analyze_transaction_for_arbitrage(self, tx_data: Dict) -> Tuple[bool, Optional[Dict]]:
"""
Analyze transaction data for potential arbitrage using instruction-level parsing
Returns a tuple of (is_arbitrage, arbitrage_data)
"""
from app.parsers.parser_registry import parser_registry
# Check if transaction exists and has data
if not tx_data or "transaction" not in tx_data:
return False, None
# Parse all instructions in the transaction
parsed_instructions = parser_registry.parse_transaction_instructions(tx_data)
# Extract swap information from parsed instructions
swaps = []
# Track token flow for a single user wallet
token_flow = {} # token_address -> net_amount
user_wallet = None
for instruction in parsed_instructions:
program_id = instruction.get("program_id", "")
parser = parser_registry.get_parser_for_program(program_id)
if not parser:
continue
# Extract swap info if the parser is a SwapParser
if hasattr(parser, "extract_swap_info"):
try:
token_in, token_out, amount_in, amount_out = parser.extract_swap_info(instruction)
if token_in and token_out and amount_in is not None and amount_out is not None:
swaps.append({
"token_in": token_in,
"token_out": token_out,
"amount_in": amount_in,
"amount_out": amount_out,
"program_id": program_id,
})
# Track net token flow
token_flow[token_in] = token_flow.get(token_in, 0) - amount_in
token_flow[token_out] = token_flow.get(token_out, 0) + amount_out
# Try to identify user wallet
if "user_wallet" in instruction and not user_wallet:
user_wallet = instruction.get("user_wallet")
except Exception as e:
logger.error(f"Error extracting swap info: {str(e)}")
# No need to continue if we don't have enough swaps
if len(swaps) < 3:
return False, None
# Check for circular swaps (token A -> token B -> token C -> ... -> token A)
# This is a key arbitrage pattern
token_path = []
for swap in swaps:
token_path.append(swap["token_in"])
token_path.append(swap["token_out"])
# Check if the first and last tokens in the path are the same
is_circular = len(token_path) >= 2 and token_path[0] == token_path[-1]
# Check if there's a profit in any token
profit_token = None
profit_amount = 0
for token, amount in token_flow.items():
if amount > 0:
# If we have a net positive amount of any token, it's a profit
if profit_amount < amount:
profit_token = token
profit_amount = amount
# Determine arbitrage confidence score
confidence_score = 0.0
if is_circular and profit_token:
# High confidence if we have a circular path and profit
confidence_score = 0.9
elif is_circular:
# Medium confidence if we have a circular path but no clear profit
confidence_score = 0.7
elif profit_token:
# Lower confidence if we have profit but no circular path
confidence_score = 0.5
else:
# Not likely an arbitrage
return False, None
# Consider it arbitrage if we have high enough confidence
if confidence_score >= 0.5:
return True, {
"profit_token_address": profit_token or swaps[0]["token_in"],
"profit_amount": profit_amount or 0.01, # Fallback to placeholder if we can't determine
"path": json.dumps(token_path),
"confidence_score": confidence_score,
}
return False, None