removed the fucking vendor files
This commit is contained in:
409
pkg/arbitrage/database.go
Normal file
409
pkg/arbitrage/database.go
Normal file
@@ -0,0 +1,409 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// SQLiteDatabase implements ArbitrageDatabase using SQLite
|
||||
type SQLiteDatabase struct {
|
||||
db *sql.DB
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
// NewSQLiteDatabase creates a new SQLite database for arbitrage data
|
||||
func NewSQLiteDatabase(dbPath string, logger *logger.Logger) (*SQLiteDatabase, error) {
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
database := &SQLiteDatabase{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if err := database.createTables(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create tables: %w", err)
|
||||
}
|
||||
|
||||
return database, nil
|
||||
}
|
||||
|
||||
// createTables creates the necessary database tables
|
||||
func (db *SQLiteDatabase) createTables() error {
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS arbitrage_opportunities (
|
||||
id TEXT PRIMARY KEY,
|
||||
path_json TEXT NOT NULL,
|
||||
trigger_event_json TEXT NOT NULL,
|
||||
detected_at INTEGER NOT NULL,
|
||||
estimated_profit TEXT NOT NULL,
|
||||
required_amount TEXT NOT NULL,
|
||||
urgency INTEGER NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS arbitrage_executions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
opportunity_id TEXT,
|
||||
transaction_hash TEXT NOT NULL,
|
||||
gas_used INTEGER NOT NULL,
|
||||
gas_price TEXT NOT NULL,
|
||||
profit_realized TEXT NOT NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
error_message TEXT,
|
||||
execution_time_ms INTEGER NOT NULL,
|
||||
path_json TEXT NOT NULL,
|
||||
executed_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY(opportunity_id) REFERENCES arbitrage_opportunities(id)
|
||||
)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS pool_data (
|
||||
address TEXT PRIMARY KEY,
|
||||
token0 TEXT NOT NULL,
|
||||
token1 TEXT NOT NULL,
|
||||
protocol TEXT NOT NULL,
|
||||
fee INTEGER NOT NULL,
|
||||
liquidity TEXT NOT NULL,
|
||||
sqrt_price_x96 TEXT NOT NULL,
|
||||
tick INTEGER,
|
||||
block_number INTEGER NOT NULL,
|
||||
transaction_hash TEXT NOT NULL,
|
||||
log_index INTEGER NOT NULL,
|
||||
last_updated INTEGER NOT NULL,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS swap_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
transaction_hash TEXT NOT NULL,
|
||||
pool_address TEXT NOT NULL,
|
||||
token0 TEXT NOT NULL,
|
||||
token1 TEXT NOT NULL,
|
||||
amount0 TEXT NOT NULL,
|
||||
amount1 TEXT NOT NULL,
|
||||
sqrt_price_x96 TEXT NOT NULL,
|
||||
liquidity TEXT NOT NULL,
|
||||
tick INTEGER,
|
||||
block_number INTEGER NOT NULL,
|
||||
log_index INTEGER NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)`,
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
if _, err := db.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create indexes for better performance
|
||||
indexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_opportunities_detected_at ON arbitrage_opportunities(detected_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_opportunities_urgency ON arbitrage_opportunities(urgency)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_executions_executed_at ON arbitrage_executions(executed_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_executions_success ON arbitrage_executions(success)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_pool_data_updated ON pool_data(last_updated)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_pool_data_tokens ON pool_data(token0, token1)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_swap_events_pool ON swap_events(pool_address)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_swap_events_block ON swap_events(block_number)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_swap_events_timestamp ON swap_events(timestamp)`,
|
||||
}
|
||||
|
||||
for _, index := range indexes {
|
||||
if _, err := db.db.Exec(index); err != nil {
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveOpportunity saves an arbitrage opportunity to the database
|
||||
func (db *SQLiteDatabase) SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error {
|
||||
pathJSON, err := json.Marshal(opportunity.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal path: %w", err)
|
||||
}
|
||||
|
||||
eventJSON, err := json.Marshal(opportunity.TriggerEvent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal trigger event: %w", err)
|
||||
}
|
||||
|
||||
query := `INSERT INTO arbitrage_opportunities
|
||||
(id, path_json, trigger_event_json, detected_at, estimated_profit, required_amount, urgency, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
_, err = db.db.ExecContext(ctx, query,
|
||||
opportunity.ID,
|
||||
string(pathJSON),
|
||||
string(eventJSON),
|
||||
opportunity.DetectedAt.Unix(),
|
||||
opportunity.EstimatedProfit.String(),
|
||||
opportunity.RequiredAmount.String(),
|
||||
opportunity.Urgency,
|
||||
opportunity.ExpiresAt.Unix(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save opportunity: %w", err)
|
||||
}
|
||||
|
||||
db.logger.Debug(fmt.Sprintf("Saved arbitrage opportunity %s to database", opportunity.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveExecution saves an arbitrage execution result to the database
|
||||
func (db *SQLiteDatabase) SaveExecution(ctx context.Context, result *ExecutionResult) error {
|
||||
pathJSON, err := json.Marshal(result.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal path: %w", err)
|
||||
}
|
||||
|
||||
var opportunityID *string
|
||||
if result.Path != nil {
|
||||
// Try to find the opportunity ID from the path
|
||||
// This is a simplified approach - in production you'd want better linking
|
||||
id := generateOpportunityIDFromPath(result.Path)
|
||||
opportunityID = &id
|
||||
}
|
||||
|
||||
var errorMessage *string
|
||||
if result.Error != nil {
|
||||
msg := result.Error.Error()
|
||||
errorMessage = &msg
|
||||
}
|
||||
|
||||
query := `INSERT INTO arbitrage_executions
|
||||
(opportunity_id, transaction_hash, gas_used, gas_price, profit_realized, success,
|
||||
error_message, execution_time_ms, path_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
_, err = db.db.ExecContext(ctx, query,
|
||||
opportunityID,
|
||||
result.TransactionHash.Hex(),
|
||||
result.GasUsed,
|
||||
result.GasPrice.String(),
|
||||
result.ProfitRealized.String(),
|
||||
result.Success,
|
||||
errorMessage,
|
||||
result.ExecutionTime.Milliseconds(),
|
||||
string(pathJSON),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save execution: %w", err)
|
||||
}
|
||||
|
||||
db.logger.Debug(fmt.Sprintf("Saved arbitrage execution %s to database", result.TransactionHash.Hex()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExecutionHistory retrieves historical arbitrage executions
|
||||
func (db *SQLiteDatabase) GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error) {
|
||||
query := `SELECT transaction_hash, gas_used, gas_price, profit_realized, success,
|
||||
error_message, execution_time_ms, path_json, executed_at
|
||||
FROM arbitrage_executions
|
||||
ORDER BY executed_at DESC
|
||||
LIMIT ?`
|
||||
|
||||
rows, err := db.db.QueryContext(ctx, query, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query execution history: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []*ExecutionResult
|
||||
for rows.Next() {
|
||||
var txHashStr, gasPriceStr, profitStr, pathJSON string
|
||||
var gasUsed, executionTimeMs, executedAt int64
|
||||
var success bool
|
||||
var errorMessage *string
|
||||
|
||||
err := rows.Scan(&txHashStr, &gasUsed, &gasPriceStr, &profitStr, &success,
|
||||
&errorMessage, &executionTimeMs, &pathJSON, &executedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan execution row: %w", err)
|
||||
}
|
||||
|
||||
// Parse path JSON
|
||||
var path ArbitragePath
|
||||
if err := json.Unmarshal([]byte(pathJSON), &path); err != nil {
|
||||
db.logger.Warn(fmt.Sprintf("Failed to unmarshal path JSON: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
result := &ExecutionResult{
|
||||
TransactionHash: common.HexToHash(txHashStr),
|
||||
GasUsed: uint64(gasUsed),
|
||||
Success: success,
|
||||
ExecutionTime: time.Duration(executionTimeMs) * time.Millisecond,
|
||||
Path: &path,
|
||||
}
|
||||
|
||||
// Parse gas price and profit
|
||||
result.GasPrice, _ = parseBigInt(gasPriceStr)
|
||||
result.ProfitRealized, _ = parseBigInt(profitStr)
|
||||
|
||||
// Parse error message
|
||||
if errorMessage != nil {
|
||||
result.Error = fmt.Errorf(*errorMessage)
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// SavePoolData saves pool data to the database
|
||||
func (db *SQLiteDatabase) SavePoolData(ctx context.Context, poolData *SimplePoolData) error {
|
||||
query := `INSERT OR REPLACE INTO pool_data
|
||||
(address, token0, token1, protocol, fee, liquidity, sqrt_price_x96, tick,
|
||||
block_number, transaction_hash, log_index, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
_, err := db.db.ExecContext(ctx, query,
|
||||
poolData.Address.Hex(),
|
||||
poolData.Token0.Hex(),
|
||||
poolData.Token1.Hex(),
|
||||
"UniswapV3", // Default protocol
|
||||
poolData.Fee,
|
||||
poolData.Liquidity.String(),
|
||||
poolData.SqrtPriceX96.String(),
|
||||
poolData.Tick,
|
||||
poolData.BlockNumber,
|
||||
poolData.TxHash.Hex(),
|
||||
poolData.LogIndex,
|
||||
poolData.LastUpdated.Unix(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save pool data: %w", err)
|
||||
}
|
||||
|
||||
db.logger.Debug(fmt.Sprintf("Saved pool data %s to database", poolData.Address.Hex()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPoolData retrieves pool data from the database
|
||||
func (db *SQLiteDatabase) GetPoolData(ctx context.Context, poolAddress common.Address) (*SimplePoolData, error) {
|
||||
query := `SELECT address, token0, token1, protocol, fee, liquidity, sqrt_price_x96, tick,
|
||||
block_number, transaction_hash, log_index, last_updated
|
||||
FROM pool_data WHERE address = ?`
|
||||
|
||||
row := db.db.QueryRowContext(ctx, query, poolAddress.Hex())
|
||||
|
||||
var addrStr, token0Str, token1Str, protocol, liquidityStr, sqrtPriceStr, txHashStr string
|
||||
var fee, tick, blockNumber, logIndex, lastUpdated int64
|
||||
|
||||
err := row.Scan(&addrStr, &token0Str, &token1Str, &protocol, &fee,
|
||||
&liquidityStr, &sqrtPriceStr, &tick, &blockNumber, &txHashStr, &logIndex, &lastUpdated)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("pool data not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to query pool data: %w", err)
|
||||
}
|
||||
|
||||
// Parse the data
|
||||
liquidity, ok1 := parseBigInt(liquidityStr)
|
||||
sqrtPriceX96, ok2 := parseBigInt(sqrtPriceStr)
|
||||
|
||||
if !ok1 || !ok2 {
|
||||
return nil, fmt.Errorf("failed to parse pool numeric data")
|
||||
}
|
||||
|
||||
poolData := &SimplePoolData{
|
||||
Address: common.HexToAddress(addrStr),
|
||||
Token0: common.HexToAddress(token0Str),
|
||||
Token1: common.HexToAddress(token1Str),
|
||||
Fee: fee,
|
||||
Liquidity: liquidity,
|
||||
SqrtPriceX96: sqrtPriceX96,
|
||||
Tick: int32(tick),
|
||||
BlockNumber: uint64(blockNumber),
|
||||
TxHash: common.HexToHash(txHashStr),
|
||||
LogIndex: uint(logIndex),
|
||||
LastUpdated: time.Unix(lastUpdated, 0),
|
||||
}
|
||||
|
||||
return poolData, nil
|
||||
}
|
||||
|
||||
// GetOpportunityCount returns the total number of opportunities detected
|
||||
func (db *SQLiteDatabase) GetOpportunityCount(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := db.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM arbitrage_opportunities").Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetExecutionCount returns the total number of executions attempted
|
||||
func (db *SQLiteDatabase) GetExecutionCount(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := db.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM arbitrage_executions").Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetSuccessfulExecutionCount returns the number of successful executions
|
||||
func (db *SQLiteDatabase) GetSuccessfulExecutionCount(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := db.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM arbitrage_executions WHERE success = 1").Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetTotalProfit returns the total profit realized from all successful executions
|
||||
func (db *SQLiteDatabase) GetTotalProfit(ctx context.Context) (string, error) {
|
||||
var totalProfit sql.NullString
|
||||
err := db.db.QueryRowContext(ctx,
|
||||
"SELECT SUM(CAST(profit_realized AS REAL)) FROM arbitrage_executions WHERE success = 1").Scan(&totalProfit)
|
||||
|
||||
if err != nil {
|
||||
return "0", err
|
||||
}
|
||||
|
||||
if !totalProfit.Valid {
|
||||
return "0", nil
|
||||
}
|
||||
|
||||
return totalProfit.String, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *SQLiteDatabase) Close() error {
|
||||
return db.db.Close()
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// parseBigInt safely parses a big integer from string
|
||||
func parseBigInt(s string) (*big.Int, bool) {
|
||||
result := new(big.Int)
|
||||
result, ok := result.SetString(s, 10)
|
||||
return result, ok
|
||||
}
|
||||
|
||||
// generateOpportunityIDFromPath generates a consistent ID from path data
|
||||
func generateOpportunityIDFromPath(path *ArbitragePath) string {
|
||||
if path == nil || len(path.Tokens) == 0 {
|
||||
return fmt.Sprintf("unknown_%d", time.Now().UnixNano())
|
||||
}
|
||||
return fmt.Sprintf("path_%s_%d", path.Tokens[0].Hex()[:8], time.Now().UnixNano())
|
||||
}
|
||||
699
pkg/arbitrage/executor.go
Normal file
699
pkg/arbitrage/executor.go
Normal file
@@ -0,0 +1,699 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/fraktal/mev-beta/bindings/arbitrage"
|
||||
"github.com/fraktal/mev-beta/bindings/flashswap"
|
||||
"github.com/fraktal/mev-beta/bindings/tokens"
|
||||
"github.com/fraktal/mev-beta/bindings/uniswap"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/mev"
|
||||
"github.com/fraktal/mev-beta/pkg/security"
|
||||
)
|
||||
|
||||
// ArbitrageExecutor manages the execution of arbitrage opportunities using smart contracts
|
||||
type ArbitrageExecutor struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
keyManager *security.KeyManager
|
||||
competitionAnalyzer *mev.CompetitionAnalyzer
|
||||
|
||||
// Contract instances
|
||||
arbitrageContract *arbitrage.ArbitrageExecutor
|
||||
flashSwapContract *flashswap.BaseFlashSwapper
|
||||
|
||||
// Contract addresses
|
||||
arbitrageAddress common.Address
|
||||
flashSwapAddress common.Address
|
||||
|
||||
// Configuration
|
||||
maxGasPrice *big.Int
|
||||
maxGasLimit uint64
|
||||
slippageTolerance float64
|
||||
minProfitThreshold *big.Int
|
||||
|
||||
// Transaction options
|
||||
transactOpts *bind.TransactOpts
|
||||
callOpts *bind.CallOpts
|
||||
}
|
||||
|
||||
// ExecutionResult represents the result of an arbitrage execution
|
||||
type ExecutionResult struct {
|
||||
TransactionHash common.Hash
|
||||
GasUsed uint64
|
||||
GasPrice *big.Int
|
||||
ProfitRealized *big.Int
|
||||
Success bool
|
||||
Error error
|
||||
ExecutionTime time.Duration
|
||||
Path *ArbitragePath
|
||||
}
|
||||
|
||||
// ArbitrageParams contains parameters for arbitrage execution
|
||||
type ArbitrageParams struct {
|
||||
Path *ArbitragePath
|
||||
InputAmount *big.Int
|
||||
MinOutputAmount *big.Int
|
||||
Deadline *big.Int
|
||||
FlashSwapData []byte
|
||||
}
|
||||
|
||||
// NewArbitrageExecutor creates a new arbitrage executor
|
||||
func NewArbitrageExecutor(
|
||||
client *ethclient.Client,
|
||||
logger *logger.Logger,
|
||||
keyManager *security.KeyManager,
|
||||
arbitrageAddr common.Address,
|
||||
flashSwapAddr common.Address,
|
||||
) (*ArbitrageExecutor, error) {
|
||||
logger.Info(fmt.Sprintf("Creating arbitrage contract instance at %s", arbitrageAddr.Hex()))
|
||||
|
||||
// Create contract instances
|
||||
arbitrageContract, err := arbitrage.NewArbitrageExecutor(arbitrageAddr, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create arbitrage contract instance: %w", err)
|
||||
}
|
||||
logger.Info("Arbitrage contract instance created successfully")
|
||||
|
||||
logger.Info(fmt.Sprintf("Creating flash swap contract instance at %s", flashSwapAddr.Hex()))
|
||||
flashSwapContract, err := flashswap.NewBaseFlashSwapper(flashSwapAddr, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create flash swap contract instance: %w", err)
|
||||
}
|
||||
logger.Info("Flash swap contract instance created successfully")
|
||||
|
||||
logger.Info("Creating MEV competition analyzer...")
|
||||
// Initialize MEV competition analyzer for profitable bidding
|
||||
competitionAnalyzer := mev.NewCompetitionAnalyzer(client, logger)
|
||||
logger.Info("MEV competition analyzer created successfully")
|
||||
|
||||
logger.Info("Getting active private key from key manager...")
|
||||
|
||||
// Use a timeout to prevent hanging
|
||||
type keyResult struct {
|
||||
key *ecdsa.PrivateKey
|
||||
err error
|
||||
}
|
||||
|
||||
keyChannel := make(chan keyResult, 1)
|
||||
go func() {
|
||||
key, err := keyManager.GetActivePrivateKey()
|
||||
keyChannel <- keyResult{key, err}
|
||||
}()
|
||||
|
||||
var privateKey *ecdsa.PrivateKey
|
||||
select {
|
||||
case result := <-keyChannel:
|
||||
if result.err != nil {
|
||||
logger.Warn("⚠️ Could not get private key, will run in monitoring mode only")
|
||||
// For now, just continue without transaction capabilities
|
||||
return &ArbitrageExecutor{
|
||||
client: client,
|
||||
logger: logger,
|
||||
keyManager: keyManager,
|
||||
competitionAnalyzer: competitionAnalyzer,
|
||||
arbitrageAddress: arbitrageAddr,
|
||||
flashSwapAddress: flashSwapAddr,
|
||||
maxGasPrice: big.NewInt(5000000000), // 5 gwei
|
||||
maxGasLimit: 800000,
|
||||
slippageTolerance: 0.01, // 1%
|
||||
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
|
||||
}, nil
|
||||
}
|
||||
privateKey = result.key
|
||||
case <-time.After(5 * time.Second):
|
||||
logger.Warn("⚠️ Key retrieval timed out, will run in monitoring mode only")
|
||||
return &ArbitrageExecutor{
|
||||
client: client,
|
||||
logger: logger,
|
||||
keyManager: keyManager,
|
||||
competitionAnalyzer: competitionAnalyzer,
|
||||
arbitrageAddress: arbitrageAddr,
|
||||
flashSwapAddress: flashSwapAddr,
|
||||
maxGasPrice: big.NewInt(5000000000), // 5 gwei
|
||||
maxGasLimit: 800000,
|
||||
slippageTolerance: 0.01, // 1%
|
||||
minProfitThreshold: big.NewInt(10000000000000000), // 0.01 ETH
|
||||
}, nil
|
||||
}
|
||||
logger.Info("Active private key retrieved successfully")
|
||||
|
||||
logger.Info("Getting network ID...")
|
||||
// Create transaction options
|
||||
chainID, err := client.NetworkID(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chain ID: %w", err)
|
||||
}
|
||||
|
||||
transactOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create transactor: %w", err)
|
||||
}
|
||||
|
||||
// Set default gas parameters
|
||||
transactOpts.GasLimit = 800000 // 800k gas limit
|
||||
transactOpts.GasPrice = big.NewInt(2000000000) // 2 gwei
|
||||
|
||||
return &ArbitrageExecutor{
|
||||
client: client,
|
||||
logger: logger,
|
||||
keyManager: keyManager,
|
||||
competitionAnalyzer: competitionAnalyzer, // CRITICAL: MEV competition analysis
|
||||
arbitrageContract: arbitrageContract,
|
||||
flashSwapContract: flashSwapContract,
|
||||
arbitrageAddress: arbitrageAddr,
|
||||
flashSwapAddress: flashSwapAddr,
|
||||
maxGasPrice: big.NewInt(5000000000), // 5 gwei max (realistic for Arbitrum)
|
||||
maxGasLimit: 1500000, // 1.5M gas max (realistic for complex arbitrage)
|
||||
slippageTolerance: 0.003, // 0.3% slippage tolerance (tight for profit)
|
||||
minProfitThreshold: big.NewInt(50000000000000000), // 0.05 ETH minimum profit (realistic after gas)
|
||||
transactOpts: transactOpts,
|
||||
callOpts: &bind.CallOpts{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExecuteArbitrage executes an arbitrage opportunity using flash swaps with MEV competition analysis
|
||||
func (ae *ArbitrageExecutor) ExecuteArbitrage(ctx context.Context, params *ArbitrageParams) (*ExecutionResult, error) {
|
||||
// Create MEV opportunity for competition analysis
|
||||
opportunity := &mev.MEVOpportunity{
|
||||
TxHash: "", // Will be filled after execution
|
||||
Block: 0, // Current block
|
||||
OpportunityType: "arbitrage",
|
||||
EstimatedProfit: big.NewInt(1000000000000000000), // 1 ETH default, will be calculated properly
|
||||
RequiredGas: 800000, // Estimated gas for arbitrage
|
||||
}
|
||||
|
||||
// Analyze MEV competition
|
||||
competition, err := ae.competitionAnalyzer.AnalyzeCompetition(ctx, opportunity)
|
||||
if err != nil {
|
||||
ae.logger.Warn(fmt.Sprintf("Competition analysis failed, proceeding with default strategy: %v", err))
|
||||
// Continue with default execution
|
||||
}
|
||||
|
||||
// Calculate optimal bidding strategy
|
||||
var biddingStrategy *mev.BiddingStrategy
|
||||
if competition != nil {
|
||||
biddingStrategy, err = ae.competitionAnalyzer.CalculateOptimalBid(ctx, opportunity, competition)
|
||||
if err != nil {
|
||||
ae.logger.Error(fmt.Sprintf("Failed to calculate optimal bid: %v", err))
|
||||
return nil, fmt.Errorf("arbitrage not profitable with competitive gas pricing: %w", err)
|
||||
}
|
||||
|
||||
// Update transaction options with competitive gas pricing
|
||||
ae.transactOpts.GasPrice = biddingStrategy.PriorityFee
|
||||
ae.transactOpts.GasLimit = biddingStrategy.GasLimit
|
||||
|
||||
ae.logger.Info(fmt.Sprintf("MEV Strategy: Priority fee: %s gwei, Success rate: %.1f%%, Net profit expected: %s ETH",
|
||||
formatGweiFromWei(biddingStrategy.PriorityFee),
|
||||
biddingStrategy.SuccessProbability*100,
|
||||
formatEtherFromWei(new(big.Int).Sub(opportunity.EstimatedProfit, biddingStrategy.TotalCost))))
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
ae.logger.Info(fmt.Sprintf("Starting arbitrage execution for path with %d hops, expected profit: %s ETH",
|
||||
len(params.Path.Pools), formatEther(params.Path.NetProfit)))
|
||||
|
||||
result := &ExecutionResult{
|
||||
Path: params.Path,
|
||||
ExecutionTime: 0,
|
||||
Success: false,
|
||||
}
|
||||
|
||||
// Pre-execution validation
|
||||
if err := ae.validateExecution(ctx, params); err != nil {
|
||||
result.Error = fmt.Errorf("validation failed: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
// Update gas price based on network conditions
|
||||
if err := ae.updateGasPrice(ctx); err != nil {
|
||||
ae.logger.Warn(fmt.Sprintf("Failed to update gas price: %v", err))
|
||||
}
|
||||
|
||||
// Prepare flash swap parameters
|
||||
flashSwapParams, err := ae.prepareFlashSwapParams(params)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to prepare flash swap parameters: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
// Execute the flash swap arbitrage
|
||||
tx, err := ae.executeFlashSwapArbitrage(ctx, flashSwapParams)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("flash swap execution failed: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
result.TransactionHash = tx.Hash()
|
||||
|
||||
// Wait for transaction confirmation
|
||||
receipt, err := ae.waitForConfirmation(ctx, tx.Hash())
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("transaction confirmation failed: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
// Process execution results
|
||||
result.GasUsed = receipt.GasUsed
|
||||
result.GasPrice = tx.GasPrice()
|
||||
result.Success = receipt.Status == types.ReceiptStatusSuccessful
|
||||
|
||||
if result.Success {
|
||||
// Calculate actual profit
|
||||
actualProfit, err := ae.calculateActualProfit(ctx, receipt)
|
||||
if err != nil {
|
||||
ae.logger.Warn(fmt.Sprintf("Failed to calculate actual profit: %v", err))
|
||||
actualProfit = params.Path.NetProfit // Fallback to estimated
|
||||
}
|
||||
result.ProfitRealized = actualProfit
|
||||
|
||||
ae.logger.Info(fmt.Sprintf("Arbitrage execution successful! TX: %s, Gas used: %d, Profit: %s ETH",
|
||||
result.TransactionHash.Hex(), result.GasUsed, formatEther(result.ProfitRealized)))
|
||||
} else {
|
||||
result.Error = fmt.Errorf("transaction failed with status %d", receipt.Status)
|
||||
ae.logger.Error(fmt.Sprintf("Arbitrage execution failed! TX: %s, Gas used: %d",
|
||||
result.TransactionHash.Hex(), result.GasUsed))
|
||||
}
|
||||
|
||||
result.ExecutionTime = time.Since(start)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
// validateExecution validates the arbitrage execution parameters
|
||||
func (ae *ArbitrageExecutor) validateExecution(ctx context.Context, params *ArbitrageParams) error {
|
||||
// Check minimum profit threshold
|
||||
if params.Path.NetProfit.Cmp(ae.minProfitThreshold) < 0 {
|
||||
return fmt.Errorf("profit %s below minimum threshold %s",
|
||||
formatEther(params.Path.NetProfit), formatEther(ae.minProfitThreshold))
|
||||
}
|
||||
|
||||
// Validate path has at least 2 hops
|
||||
if len(params.Path.Pools) < 2 {
|
||||
return fmt.Errorf("arbitrage path must have at least 2 hops")
|
||||
}
|
||||
|
||||
// Check token balances if needed
|
||||
for i, pool := range params.Path.Pools {
|
||||
if err := ae.validatePoolLiquidity(ctx, pool, params.InputAmount); err != nil {
|
||||
return fmt.Errorf("pool %d validation failed: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check gas price is reasonable
|
||||
currentGasPrice, err := ae.client.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current gas price: %w", err)
|
||||
}
|
||||
|
||||
if currentGasPrice.Cmp(ae.maxGasPrice) > 0 {
|
||||
return fmt.Errorf("gas price too high: %s > %s", currentGasPrice.String(), ae.maxGasPrice.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePoolLiquidity validates that a pool has sufficient liquidity
|
||||
func (ae *ArbitrageExecutor) validatePoolLiquidity(ctx context.Context, pool *PoolInfo, amount *big.Int) error {
|
||||
// Create ERC20 contract instance to check pool reserves
|
||||
token0Contract, err := tokens.NewIERC20(pool.Token0, ae.client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create token0 contract: %w", err)
|
||||
}
|
||||
|
||||
token1Contract, err := tokens.NewIERC20(pool.Token1, ae.client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create token1 contract: %w", err)
|
||||
}
|
||||
|
||||
// Check balances of the pool
|
||||
balance0, err := token0Contract.BalanceOf(ae.callOpts, pool.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get token0 balance: %w", err)
|
||||
}
|
||||
|
||||
balance1, err := token1Contract.BalanceOf(ae.callOpts, pool.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get token1 balance: %w", err)
|
||||
}
|
||||
|
||||
// Ensure sufficient liquidity (at least 10x the swap amount)
|
||||
minLiquidity := new(big.Int).Mul(amount, big.NewInt(10))
|
||||
if balance0.Cmp(minLiquidity) < 0 && balance1.Cmp(minLiquidity) < 0 {
|
||||
return fmt.Errorf("insufficient liquidity: balance0=%s, balance1=%s, required=%s",
|
||||
balance0.String(), balance1.String(), minLiquidity.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareFlashSwapParams prepares parameters for the flash swap execution
|
||||
func (ae *ArbitrageExecutor) prepareFlashSwapParams(params *ArbitrageParams) (*FlashSwapParams, error) {
|
||||
// Build the swap path for the flash swap contract
|
||||
path := make([]common.Address, len(params.Path.Tokens))
|
||||
copy(path, params.Path.Tokens)
|
||||
|
||||
// Build pool addresses
|
||||
pools := make([]common.Address, len(params.Path.Pools))
|
||||
for i, pool := range params.Path.Pools {
|
||||
pools[i] = pool.Address
|
||||
}
|
||||
|
||||
// Build fee array
|
||||
fees := make([]*big.Int, len(params.Path.Fees))
|
||||
for i, fee := range params.Path.Fees {
|
||||
fees[i] = big.NewInt(fee)
|
||||
}
|
||||
|
||||
// Calculate minimum output with slippage tolerance
|
||||
slippageMultiplier := big.NewFloat(1.0 - ae.slippageTolerance)
|
||||
expectedOutputFloat := new(big.Float).SetInt(params.MinOutputAmount)
|
||||
minOutputFloat := new(big.Float).Mul(expectedOutputFloat, slippageMultiplier)
|
||||
minOutput := new(big.Int)
|
||||
minOutputFloat.Int(minOutput)
|
||||
|
||||
return &FlashSwapParams{
|
||||
TokenPath: path,
|
||||
PoolPath: pools,
|
||||
Fees: fees,
|
||||
AmountIn: params.InputAmount,
|
||||
MinAmountOut: minOutput,
|
||||
Deadline: params.Deadline,
|
||||
FlashSwapData: params.FlashSwapData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FlashSwapParams contains parameters for flash swap execution
|
||||
type FlashSwapParams struct {
|
||||
TokenPath []common.Address
|
||||
PoolPath []common.Address
|
||||
Fees []*big.Int
|
||||
AmountIn *big.Int
|
||||
MinAmountOut *big.Int
|
||||
Deadline *big.Int
|
||||
FlashSwapData []byte
|
||||
}
|
||||
|
||||
// executeFlashSwapArbitrage executes the flash swap arbitrage transaction
|
||||
func (ae *ArbitrageExecutor) executeFlashSwapArbitrage(ctx context.Context, params *FlashSwapParams) (*types.Transaction, error) {
|
||||
// Set deadline if not provided (5 minutes from now)
|
||||
if params.Deadline == nil {
|
||||
params.Deadline = big.NewInt(time.Now().Add(5 * time.Minute).Unix())
|
||||
}
|
||||
|
||||
// Estimate gas limit
|
||||
gasLimit, err := ae.estimateGasForArbitrage(ctx, params)
|
||||
if err != nil {
|
||||
ae.logger.Warn(fmt.Sprintf("Gas estimation failed, using default: %v", err))
|
||||
gasLimit = ae.maxGasLimit
|
||||
}
|
||||
|
||||
ae.transactOpts.GasLimit = gasLimit
|
||||
ae.transactOpts.Context = ctx
|
||||
|
||||
ae.logger.Debug(fmt.Sprintf("Executing flash swap with params: tokens=%v, pools=%v, amountIn=%s, minOut=%s, gasLimit=%d",
|
||||
params.TokenPath, params.PoolPath, params.AmountIn.String(), params.MinAmountOut.String(), gasLimit))
|
||||
|
||||
// Execute the arbitrage through the deployed Uniswap V3 pool using flash swap
|
||||
// We'll use the Uniswap V3 pool directly for flash swaps since it's already deployed
|
||||
|
||||
// Get the pool address for the first pair in the path
|
||||
poolAddress := params.PoolPath[0]
|
||||
|
||||
// Create flash swap parameters
|
||||
flashSwapParams := flashswap.IFlashSwapperFlashSwapParams{
|
||||
Token0: params.TokenPath[0],
|
||||
Token1: params.TokenPath[1],
|
||||
Amount0: params.AmountIn,
|
||||
Amount1: big.NewInt(0), // We only need one token for flash swap
|
||||
To: ae.transactOpts.From, // Send back to our account
|
||||
Data: []byte{}, // Encode arbitrage data if needed
|
||||
}
|
||||
|
||||
// Execute flash swap using Uniswap V3 pool
|
||||
tx, err := ae.executeUniswapV3FlashSwap(ctx, poolAddress, flashSwapParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute Uniswap V3 flash swap: %w", err)
|
||||
}
|
||||
|
||||
ae.logger.Info(fmt.Sprintf("Flash swap transaction submitted: %s", tx.Hash().Hex()))
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// estimateGasForArbitrage estimates gas needed for the arbitrage transaction
|
||||
func (ae *ArbitrageExecutor) estimateGasForArbitrage(ctx context.Context, params *FlashSwapParams) (uint64, error) {
|
||||
// For now, return a conservative estimate
|
||||
// In production, this would call the contract's estimateGas method
|
||||
estimatedGas := uint64(500000) // 500k gas conservative estimate
|
||||
|
||||
// Add 20% buffer to estimated gas
|
||||
gasWithBuffer := estimatedGas + (estimatedGas * 20 / 100)
|
||||
|
||||
if gasWithBuffer > ae.maxGasLimit {
|
||||
gasWithBuffer = ae.maxGasLimit
|
||||
}
|
||||
|
||||
return gasWithBuffer, nil
|
||||
}
|
||||
|
||||
// updateGasPrice updates gas price based on network conditions
|
||||
func (ae *ArbitrageExecutor) updateGasPrice(ctx context.Context) error {
|
||||
gasPrice, err := ae.client.SuggestGasPrice(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply 10% premium for faster execution
|
||||
premiumGasPrice := new(big.Int).Add(gasPrice, new(big.Int).Div(gasPrice, big.NewInt(10)))
|
||||
|
||||
if premiumGasPrice.Cmp(ae.maxGasPrice) > 0 {
|
||||
premiumGasPrice = ae.maxGasPrice
|
||||
}
|
||||
|
||||
ae.transactOpts.GasPrice = premiumGasPrice
|
||||
ae.logger.Debug(fmt.Sprintf("Updated gas price to %s wei", premiumGasPrice.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForConfirmation waits for transaction confirmation
|
||||
func (ae *ArbitrageExecutor) waitForConfirmation(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
|
||||
timeout := 30 * time.Second
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("transaction confirmation timeout")
|
||||
case <-ticker.C:
|
||||
receipt, err := ae.client.TransactionReceipt(ctx, txHash)
|
||||
if err == nil {
|
||||
return receipt, nil
|
||||
}
|
||||
// Continue waiting if transaction is not yet mined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculateActualProfit calculates the actual profit from transaction receipt
|
||||
func (ae *ArbitrageExecutor) calculateActualProfit(ctx context.Context, receipt *types.Receipt) (*big.Int, error) {
|
||||
// Parse logs to find profit events
|
||||
for _, log := range receipt.Logs {
|
||||
if log.Address == ae.arbitrageAddress {
|
||||
// Parse arbitrage execution event
|
||||
event, err := ae.arbitrageContract.ParseArbitrageExecuted(*log)
|
||||
if err != nil {
|
||||
continue // Not the event we're looking for
|
||||
}
|
||||
return event.Profit, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If no event found, calculate from balance changes
|
||||
return ae.calculateProfitFromBalanceChange(ctx, receipt)
|
||||
}
|
||||
|
||||
// calculateProfitFromBalanceChange calculates REAL profit from balance changes
|
||||
func (ae *ArbitrageExecutor) calculateProfitFromBalanceChange(ctx context.Context, receipt *types.Receipt) (*big.Int, error) {
|
||||
// Parse ArbitrageExecuted event from transaction receipt
|
||||
for _, log := range receipt.Logs {
|
||||
if len(log.Topics) >= 4 && log.Topics[0].Hex() == "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925" {
|
||||
// ArbitrageExecuted event signature
|
||||
// topic[1] = tokenA, topic[2] = tokenB
|
||||
// data contains: amountIn, profit, gasUsed
|
||||
|
||||
if len(log.Data) >= 96 { // 3 * 32 bytes
|
||||
amountIn := new(big.Int).SetBytes(log.Data[0:32])
|
||||
profit := new(big.Int).SetBytes(log.Data[32:64])
|
||||
gasUsed := new(big.Int).SetBytes(log.Data[64:96])
|
||||
|
||||
ae.logger.Info(fmt.Sprintf("Arbitrage executed - AmountIn: %s, Profit: %s, Gas: %s",
|
||||
amountIn.String(), profit.String(), gasUsed.String()))
|
||||
|
||||
// Verify profit covers gas costs
|
||||
gasCost := new(big.Int).Mul(gasUsed, receipt.EffectiveGasPrice)
|
||||
netProfit := new(big.Int).Sub(profit, gasCost)
|
||||
|
||||
if netProfit.Sign() > 0 {
|
||||
ae.logger.Info(fmt.Sprintf("PROFITABLE ARBITRAGE - Net profit: %s ETH",
|
||||
formatEther(netProfit)))
|
||||
return netProfit, nil
|
||||
} else {
|
||||
ae.logger.Warn(fmt.Sprintf("UNPROFITABLE ARBITRAGE - Loss: %s ETH",
|
||||
formatEther(new(big.Int).Neg(netProfit))))
|
||||
return big.NewInt(0), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no event found, this means the arbitrage failed or was unprofitable
|
||||
ae.logger.Warn("No ArbitrageExecuted event found - transaction likely failed")
|
||||
return big.NewInt(0), fmt.Errorf("no arbitrage execution detected in transaction")
|
||||
}
|
||||
|
||||
// formatEther converts wei to ETH string with 6 decimal places
|
||||
func formatEther(wei *big.Int) string {
|
||||
if wei == nil {
|
||||
return "0.000000"
|
||||
}
|
||||
eth := new(big.Float).SetInt(wei)
|
||||
eth.Quo(eth, big.NewFloat(1e18))
|
||||
return fmt.Sprintf("%.6f", eth)
|
||||
}
|
||||
|
||||
// formatEtherFromWei is an alias for formatEther for consistency
|
||||
func formatEtherFromWei(wei *big.Int) string {
|
||||
return formatEther(wei)
|
||||
}
|
||||
|
||||
// formatGweiFromWei converts wei to gwei string
|
||||
func formatGweiFromWei(wei *big.Int) string {
|
||||
if wei == nil {
|
||||
return "0.0"
|
||||
}
|
||||
gwei := new(big.Float).SetInt(wei)
|
||||
gwei.Quo(gwei, big.NewFloat(1e9))
|
||||
return fmt.Sprintf("%.2f", gwei)
|
||||
}
|
||||
|
||||
// GetArbitrageHistory retrieves historical arbitrage executions
|
||||
func (ae *ArbitrageExecutor) GetArbitrageHistory(ctx context.Context, fromBlock, toBlock *big.Int) ([]*ArbitrageEvent, error) {
|
||||
// For now, return empty slice - would need actual contract events
|
||||
// In production, this would parse the contract events properly
|
||||
var events []*ArbitrageEvent
|
||||
|
||||
ae.logger.Debug(fmt.Sprintf("Fetching arbitrage history from block %s to %s", fromBlock.String(), toBlock.String()))
|
||||
|
||||
// Placeholder implementation
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// ArbitrageEvent represents a historical arbitrage event
|
||||
type ArbitrageEvent struct {
|
||||
TransactionHash common.Hash
|
||||
BlockNumber uint64
|
||||
TokenIn common.Address
|
||||
TokenOut common.Address
|
||||
AmountIn *big.Int
|
||||
AmountOut *big.Int
|
||||
Profit *big.Int
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Helper method to check if execution is profitable after gas costs
|
||||
func (ae *ArbitrageExecutor) IsProfitableAfterGas(path *ArbitragePath, gasPrice *big.Int) bool {
|
||||
gasCost := new(big.Int).Mul(gasPrice, big.NewInt(int64(path.EstimatedGas.Uint64())))
|
||||
netProfit := new(big.Int).Sub(path.NetProfit, gasCost)
|
||||
return netProfit.Cmp(ae.minProfitThreshold) > 0
|
||||
}
|
||||
|
||||
// SetConfiguration updates executor configuration
|
||||
func (ae *ArbitrageExecutor) SetConfiguration(config *ExecutorConfig) {
|
||||
if config.MaxGasPrice != nil {
|
||||
ae.maxGasPrice = config.MaxGasPrice
|
||||
}
|
||||
if config.MaxGasLimit > 0 {
|
||||
ae.maxGasLimit = config.MaxGasLimit
|
||||
}
|
||||
if config.SlippageTolerance > 0 {
|
||||
ae.slippageTolerance = config.SlippageTolerance
|
||||
}
|
||||
if config.MinProfitThreshold != nil {
|
||||
ae.minProfitThreshold = config.MinProfitThreshold
|
||||
}
|
||||
}
|
||||
|
||||
// executeUniswapV3FlashSwap executes a flash swap directly on a Uniswap V3 pool
|
||||
func (ae *ArbitrageExecutor) executeUniswapV3FlashSwap(ctx context.Context, poolAddress common.Address, params flashswap.IFlashSwapperFlashSwapParams) (*types.Transaction, error) {
|
||||
ae.logger.Debug(fmt.Sprintf("Executing Uniswap V3 flash swap on pool %s", poolAddress.Hex()))
|
||||
|
||||
// Create pool contract instance using IUniswapV3PoolActions interface
|
||||
poolContract, err := uniswap.NewIUniswapV3PoolActions(poolAddress, ae.client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pool contract instance: %w", err)
|
||||
}
|
||||
|
||||
// For Uniswap V3, we use the pool's flash function directly
|
||||
// The callback will handle the arbitrage logic
|
||||
|
||||
// Encode the arbitrage data that will be passed to the callback
|
||||
arbitrageData, err := ae.encodeArbitrageData(params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode arbitrage data: %w", err)
|
||||
}
|
||||
|
||||
// Execute flash swap on the pool
|
||||
// amount0 > 0 means we're borrowing token0, amount1 > 0 means we're borrowing token1
|
||||
tx, err := poolContract.Flash(ae.transactOpts, ae.transactOpts.From, params.Amount0, params.Amount1, arbitrageData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("flash swap transaction failed: %w", err)
|
||||
}
|
||||
|
||||
ae.logger.Info(fmt.Sprintf("Uniswap V3 flash swap initiated: %s", tx.Hash().Hex()))
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
// encodeArbitrageData encodes the arbitrage parameters for the flash callback
|
||||
func (ae *ArbitrageExecutor) encodeArbitrageData(params flashswap.IFlashSwapperFlashSwapParams) ([]byte, error) {
|
||||
// For now, we'll encode basic parameters
|
||||
// In production, this would include the full arbitrage path and swap details
|
||||
data := struct {
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
Amount0 *big.Int
|
||||
Amount1 *big.Int
|
||||
To common.Address
|
||||
}{
|
||||
Token0: params.Token0,
|
||||
Token1: params.Token1,
|
||||
Amount0: params.Amount0,
|
||||
Amount1: params.Amount1,
|
||||
To: params.To,
|
||||
}
|
||||
|
||||
// For simplicity, we'll just return the data as JSON bytes
|
||||
// In production, you'd use proper ABI encoding
|
||||
return []byte(fmt.Sprintf(`{"token0":"%s","token1":"%s","amount0":"%s","amount1":"%s","to":"%s"}`,
|
||||
data.Token0.Hex(), data.Token1.Hex(), data.Amount0.String(), data.Amount1.String(), data.To.Hex())), nil
|
||||
}
|
||||
|
||||
// ExecutorConfig contains configuration for the arbitrage executor
|
||||
type ExecutorConfig struct {
|
||||
MaxGasPrice *big.Int
|
||||
MaxGasLimit uint64
|
||||
SlippageTolerance float64
|
||||
MinProfitThreshold *big.Int
|
||||
}
|
||||
525
pkg/arbitrage/multihop.go
Normal file
525
pkg/arbitrage/multihop.go
Normal file
@@ -0,0 +1,525 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// MultiHopScanner implements advanced multi-hop arbitrage detection
|
||||
type MultiHopScanner struct {
|
||||
logger *logger.Logger
|
||||
|
||||
// Configuration
|
||||
maxHops int // Maximum number of hops in arbitrage path
|
||||
minProfitWei *big.Int // Minimum profit threshold in wei
|
||||
maxSlippage float64 // Maximum acceptable slippage
|
||||
maxPaths int // Maximum paths to evaluate per opportunity
|
||||
pathTimeout time.Duration // Timeout for path calculation
|
||||
|
||||
// Caching
|
||||
pathCache map[string][]*ArbitragePath
|
||||
cacheMutex sync.RWMutex
|
||||
cacheExpiry time.Duration
|
||||
|
||||
// Token graph for path finding
|
||||
tokenGraph *TokenGraph
|
||||
|
||||
// Pool registry
|
||||
pools map[common.Address]*PoolInfo
|
||||
poolMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// ArbitragePath represents a complete arbitrage path
|
||||
type ArbitragePath struct {
|
||||
Tokens []common.Address // Token path (A -> B -> C -> A)
|
||||
Pools []*PoolInfo // Pools used in each hop
|
||||
Protocols []string // Protocol for each hop
|
||||
Fees []int64 // Fee for each hop
|
||||
EstimatedGas *big.Int // Estimated gas cost
|
||||
NetProfit *big.Int // Net profit after gas
|
||||
ROI float64 // Return on investment percentage
|
||||
LastUpdated time.Time // When this path was calculated
|
||||
}
|
||||
|
||||
// PoolInfo contains information about a trading pool
|
||||
type PoolInfo struct {
|
||||
Address common.Address
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
Protocol string
|
||||
Fee int64
|
||||
Liquidity *uint256.Int
|
||||
SqrtPriceX96 *uint256.Int
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
// TokenGraph represents a graph of tokens connected by pools
|
||||
type TokenGraph struct {
|
||||
adjacencyList map[common.Address]map[common.Address][]*PoolInfo
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMultiHopScanner creates a new multi-hop arbitrage scanner
|
||||
func NewMultiHopScanner(logger *logger.Logger, marketMgr interface{}) *MultiHopScanner {
|
||||
return &MultiHopScanner{
|
||||
logger: logger,
|
||||
maxHops: 4, // Max 4 hops (A->B->C->D->A)
|
||||
minProfitWei: big.NewInt(1000000000000000), // 0.001 ETH minimum profit
|
||||
maxSlippage: 0.03, // 3% max slippage
|
||||
maxPaths: 100, // Evaluate top 100 paths
|
||||
pathTimeout: time.Millisecond * 500, // 500ms timeout
|
||||
pathCache: make(map[string][]*ArbitragePath),
|
||||
cacheExpiry: time.Minute * 2, // Cache for 2 minutes
|
||||
tokenGraph: NewTokenGraph(),
|
||||
pools: make(map[common.Address]*PoolInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTokenGraph creates a new token graph
|
||||
func NewTokenGraph() *TokenGraph {
|
||||
return &TokenGraph{
|
||||
adjacencyList: make(map[common.Address]map[common.Address][]*PoolInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// ScanForArbitrage scans for multi-hop arbitrage opportunities
|
||||
func (mhs *MultiHopScanner) ScanForArbitrage(ctx context.Context, triggerToken common.Address, amount *big.Int) ([]*ArbitragePath, error) {
|
||||
start := time.Now()
|
||||
|
||||
mhs.logger.Debug(fmt.Sprintf("Starting multi-hop arbitrage scan for token %s with amount %s",
|
||||
triggerToken.Hex(), amount.String()))
|
||||
|
||||
// Update token graph with latest pool data
|
||||
if err := mhs.updateTokenGraph(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to update token graph: %w", err)
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
cacheKey := fmt.Sprintf("%s_%s", triggerToken.Hex(), amount.String())
|
||||
if paths := mhs.getCachedPaths(cacheKey); paths != nil {
|
||||
mhs.logger.Debug(fmt.Sprintf("Found %d cached arbitrage paths", len(paths)))
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// Find all possible arbitrage paths starting with triggerToken
|
||||
allPaths := mhs.findArbitragePaths(ctx, triggerToken, amount)
|
||||
|
||||
// Filter and rank paths by profitability
|
||||
profitablePaths := mhs.filterProfitablePaths(allPaths)
|
||||
|
||||
// Sort by net profit descending
|
||||
sort.Slice(profitablePaths, func(i, j int) bool {
|
||||
return profitablePaths[i].NetProfit.Cmp(profitablePaths[j].NetProfit) > 0
|
||||
})
|
||||
|
||||
// Limit to top paths
|
||||
if len(profitablePaths) > mhs.maxPaths {
|
||||
profitablePaths = profitablePaths[:mhs.maxPaths]
|
||||
}
|
||||
|
||||
// Cache results
|
||||
mhs.setCachedPaths(cacheKey, profitablePaths)
|
||||
|
||||
elapsed := time.Since(start)
|
||||
mhs.logger.Info(fmt.Sprintf("Multi-hop arbitrage scan completed in %v: found %d profitable paths out of %d total paths",
|
||||
elapsed, len(profitablePaths), len(allPaths)))
|
||||
|
||||
return profitablePaths, nil
|
||||
}
|
||||
|
||||
// findArbitragePaths finds all possible arbitrage paths
|
||||
func (mhs *MultiHopScanner) findArbitragePaths(ctx context.Context, startToken common.Address, amount *big.Int) []*ArbitragePath {
|
||||
var allPaths []*ArbitragePath
|
||||
|
||||
// Use DFS to find paths that return to the start token
|
||||
visited := make(map[common.Address]bool)
|
||||
currentPath := []*PoolInfo{}
|
||||
currentTokens := []common.Address{startToken}
|
||||
|
||||
mhs.dfsArbitragePaths(ctx, startToken, startToken, amount, visited, currentPath, currentTokens, &allPaths, 0)
|
||||
|
||||
return allPaths
|
||||
}
|
||||
|
||||
// dfsArbitragePaths uses depth-first search to find arbitrage paths
|
||||
func (mhs *MultiHopScanner) dfsArbitragePaths(
|
||||
ctx context.Context,
|
||||
currentToken, targetToken common.Address,
|
||||
amount *big.Int,
|
||||
visited map[common.Address]bool,
|
||||
currentPath []*PoolInfo,
|
||||
currentTokens []common.Address,
|
||||
allPaths *[]*ArbitragePath,
|
||||
depth int,
|
||||
) {
|
||||
// Check context for cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Prevent infinite recursion
|
||||
if depth >= mhs.maxHops {
|
||||
return
|
||||
}
|
||||
|
||||
// If we're back at the start token and have made at least 2 hops, we found a cycle
|
||||
if depth > 1 && currentToken == targetToken {
|
||||
path := mhs.createArbitragePath(currentTokens, currentPath, amount)
|
||||
if path != nil {
|
||||
*allPaths = append(*allPaths, path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get adjacent tokens (tokens we can trade to from current token)
|
||||
adjacent := mhs.tokenGraph.GetAdjacentTokens(currentToken)
|
||||
|
||||
for nextToken, pools := range adjacent {
|
||||
// Skip if already visited (prevent cycles except back to start)
|
||||
if visited[nextToken] && nextToken != targetToken {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try each pool that connects currentToken to nextToken
|
||||
for _, pool := range pools {
|
||||
if !mhs.isPoolUsable(pool) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark as visited
|
||||
visited[nextToken] = true
|
||||
|
||||
// Add to current path
|
||||
newPath := append(currentPath, pool)
|
||||
newTokens := append(currentTokens, nextToken)
|
||||
|
||||
// Recursive search
|
||||
mhs.dfsArbitragePaths(ctx, nextToken, targetToken, amount, visited, newPath, newTokens, allPaths, depth+1)
|
||||
|
||||
// Backtrack
|
||||
delete(visited, nextToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// createArbitragePath creates an ArbitragePath from the given route
|
||||
func (mhs *MultiHopScanner) createArbitragePath(tokens []common.Address, pools []*PoolInfo, initialAmount *big.Int) *ArbitragePath {
|
||||
if len(tokens) < 3 || len(pools) != len(tokens)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate the output amount through the path
|
||||
currentAmount := new(big.Int).Set(initialAmount)
|
||||
protocols := make([]string, len(pools))
|
||||
fees := make([]int64, len(pools))
|
||||
totalGasCost := big.NewInt(0)
|
||||
|
||||
for i, pool := range pools {
|
||||
protocols[i] = pool.Protocol
|
||||
fees[i] = pool.Fee
|
||||
|
||||
// Calculate swap output for this hop
|
||||
outputAmount, err := mhs.calculateSwapOutput(currentAmount, pool, tokens[i], tokens[i+1])
|
||||
if err != nil {
|
||||
mhs.logger.Debug(fmt.Sprintf("Failed to calculate swap output for pool %s: %v", pool.Address.Hex(), err))
|
||||
return nil
|
||||
}
|
||||
|
||||
currentAmount = outputAmount
|
||||
|
||||
// Add estimated gas cost for this hop
|
||||
hopGasCost := mhs.estimateHopGasCost(pool.Protocol)
|
||||
totalGasCost.Add(totalGasCost, hopGasCost)
|
||||
}
|
||||
|
||||
// Calculate net profit (final amount - initial amount - gas cost)
|
||||
netProfit := new(big.Int).Sub(currentAmount, initialAmount)
|
||||
netProfit.Sub(netProfit, totalGasCost)
|
||||
|
||||
// Calculate ROI
|
||||
roi := 0.0
|
||||
if initialAmount.Cmp(big.NewInt(0)) > 0 {
|
||||
profitFloat := new(big.Float).SetInt(netProfit)
|
||||
initialFloat := new(big.Float).SetInt(initialAmount)
|
||||
roiFloat := new(big.Float).Quo(profitFloat, initialFloat)
|
||||
roi, _ = roiFloat.Float64()
|
||||
roi *= 100 // Convert to percentage
|
||||
}
|
||||
|
||||
return &ArbitragePath{
|
||||
Tokens: tokens,
|
||||
Pools: pools,
|
||||
Protocols: protocols,
|
||||
Fees: fees,
|
||||
EstimatedGas: totalGasCost,
|
||||
NetProfit: netProfit,
|
||||
ROI: roi,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// calculateSwapOutput calculates the output amount for a swap
|
||||
func (mhs *MultiHopScanner) calculateSwapOutput(amountIn *big.Int, pool *PoolInfo, tokenIn, tokenOut common.Address) (*big.Int, error) {
|
||||
// This is a simplified calculation
|
||||
// In production, you would use the exact AMM formulas for each protocol
|
||||
|
||||
if pool.SqrtPriceX96 == nil || pool.Liquidity == nil {
|
||||
return nil, fmt.Errorf("missing pool data")
|
||||
}
|
||||
|
||||
// For Uniswap V3, use the pricing formulas
|
||||
if pool.Protocol == "UniswapV3" {
|
||||
return mhs.calculateUniswapV3Output(amountIn, pool, tokenIn, tokenOut)
|
||||
}
|
||||
|
||||
// For other protocols, use simplified AMM formula
|
||||
return mhs.calculateSimpleAMMOutput(amountIn, pool, tokenIn, tokenOut)
|
||||
}
|
||||
|
||||
// calculateUniswapV3Output calculates output for Uniswap V3 pools
|
||||
func (mhs *MultiHopScanner) calculateUniswapV3Output(amountIn *big.Int, pool *PoolInfo, tokenIn, tokenOut common.Address) (*big.Int, error) {
|
||||
// Convert sqrtPriceX96 to price
|
||||
price := uniswap.SqrtPriceX96ToPrice(pool.SqrtPriceX96.ToBig())
|
||||
|
||||
// Simple approximation: amountOut = amountIn * price * (1 - fee)
|
||||
amountInFloat := new(big.Float).SetInt(amountIn)
|
||||
|
||||
var amountOut *big.Float
|
||||
if tokenIn == pool.Token0 {
|
||||
// Token0 -> Token1
|
||||
amountOut = new(big.Float).Mul(amountInFloat, price)
|
||||
} else {
|
||||
// Token1 -> Token0
|
||||
amountOut = new(big.Float).Quo(amountInFloat, price)
|
||||
}
|
||||
|
||||
// Apply fee
|
||||
feeRate := new(big.Float).SetFloat64(float64(pool.Fee) / 1000000) // fee is in basis points
|
||||
oneMinusFee := new(big.Float).Sub(big.NewFloat(1.0), feeRate)
|
||||
amountOut.Mul(amountOut, oneMinusFee)
|
||||
|
||||
// Convert back to big.Int
|
||||
result := new(big.Int)
|
||||
amountOut.Int(result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// calculateSimpleAMMOutput calculates output using proper Uniswap V2 AMM formula
|
||||
func (mhs *MultiHopScanner) calculateSimpleAMMOutput(amountIn *big.Int, pool *PoolInfo, tokenIn, tokenOut common.Address) (*big.Int, error) {
|
||||
// For Uniswap V2-style pools, we need actual reserve data
|
||||
// Since we don't have direct reserve data, we'll estimate based on liquidity and price
|
||||
|
||||
if pool.SqrtPriceX96 == nil || pool.Liquidity == nil {
|
||||
return nil, fmt.Errorf("missing pool price or liquidity data")
|
||||
}
|
||||
|
||||
// Convert sqrtPriceX96 to price (token1/token0)
|
||||
sqrtPriceX96 := pool.SqrtPriceX96.ToBig()
|
||||
price := new(big.Float)
|
||||
|
||||
// price = (sqrtPriceX96 / 2^96)^2
|
||||
q96 := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(2), big.NewInt(96), nil))
|
||||
sqrtPrice := new(big.Float).SetInt(sqrtPriceX96)
|
||||
sqrtPrice.Quo(sqrtPrice, q96)
|
||||
price.Mul(sqrtPrice, sqrtPrice)
|
||||
|
||||
// Estimate reserves from liquidity and price
|
||||
// For Uniswap V2: reserve0 * reserve1 = k, and price = reserve1/reserve0
|
||||
// So: reserve0 = sqrt(k/price), reserve1 = sqrt(k*price)
|
||||
|
||||
k := new(big.Float).SetInt(pool.Liquidity.ToBig())
|
||||
k.Mul(k, k) // k = L^2 for approximation
|
||||
|
||||
// Calculate reserves
|
||||
priceInv := new(big.Float).Quo(big.NewFloat(1.0), price)
|
||||
reserve0Float := new(big.Float).Sqrt(new(big.Float).Mul(k, priceInv))
|
||||
reserve1Float := new(big.Float).Sqrt(new(big.Float).Mul(k, price))
|
||||
|
||||
// Convert to big.Int
|
||||
reserve0 := new(big.Int)
|
||||
reserve1 := new(big.Int)
|
||||
reserve0Float.Int(reserve0)
|
||||
reserve1Float.Int(reserve1)
|
||||
|
||||
// Determine which reserves to use based on token direction
|
||||
var reserveIn, reserveOut *big.Int
|
||||
if tokenIn == pool.Token0 {
|
||||
reserveIn = reserve0
|
||||
reserveOut = reserve1
|
||||
} else {
|
||||
reserveIn = reserve1
|
||||
reserveOut = reserve0
|
||||
}
|
||||
|
||||
// Ensure reserves are not zero
|
||||
if reserveIn.Cmp(big.NewInt(0)) == 0 || reserveOut.Cmp(big.NewInt(0)) == 0 {
|
||||
return nil, fmt.Errorf("invalid reserve calculation: reserveIn=%s, reserveOut=%s", reserveIn.String(), reserveOut.String())
|
||||
}
|
||||
|
||||
// Apply Uniswap V2 AMM formula: x * y = k with fees
|
||||
// amountOut = (amountIn * (1000 - fee) * reserveOut) / (reserveIn * 1000 + amountIn * (1000 - fee))
|
||||
|
||||
// Get fee from pool (convert basis points to per-mille)
|
||||
fee := pool.Fee / 100 // Convert from basis points (3000) to per-mille (30)
|
||||
if fee > 1000 {
|
||||
fee = 30 // Default to 3% if fee seems wrong
|
||||
}
|
||||
feeMultiplier := big.NewInt(1000 - fee) // e.g., 970 for 3% fee
|
||||
|
||||
// Calculate numerator: amountIn * feeMultiplier * reserveOut
|
||||
numerator := new(big.Int).Mul(amountIn, feeMultiplier)
|
||||
numerator.Mul(numerator, reserveOut)
|
||||
|
||||
// Calculate denominator: reserveIn * 1000 + amountIn * feeMultiplier
|
||||
denominator := new(big.Int).Mul(reserveIn, big.NewInt(1000))
|
||||
temp := new(big.Int).Mul(amountIn, feeMultiplier)
|
||||
denominator.Add(denominator, temp)
|
||||
|
||||
// Calculate output amount
|
||||
amountOut := new(big.Int).Div(numerator, denominator)
|
||||
|
||||
// Sanity check: ensure amountOut is reasonable (not more than reserves)
|
||||
if amountOut.Cmp(reserveOut) >= 0 {
|
||||
return nil, fmt.Errorf("calculated output (%s) exceeds available reserves (%s)", amountOut.String(), reserveOut.String())
|
||||
}
|
||||
|
||||
return amountOut, nil
|
||||
}
|
||||
|
||||
// Additional helper methods...
|
||||
|
||||
// updateTokenGraph updates the token graph with current pool data
|
||||
func (mhs *MultiHopScanner) updateTokenGraph(ctx context.Context) error {
|
||||
// For now, create a minimal token graph with some default pools
|
||||
// In production, this would be populated from a real pool discovery service
|
||||
|
||||
mhs.tokenGraph.mutex.Lock()
|
||||
defer mhs.tokenGraph.mutex.Unlock()
|
||||
|
||||
// Clear existing graph
|
||||
mhs.tokenGraph.adjacencyList = make(map[common.Address]map[common.Address][]*PoolInfo)
|
||||
|
||||
// Add some example pools for testing (these would come from pool discovery in production)
|
||||
// This is a simplified implementation to avoid circular dependencies
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addPoolToGraph adds a pool to the token graph
|
||||
func (mhs *MultiHopScanner) addPoolToGraph(pool *PoolInfo) {
|
||||
// Add bidirectional edges
|
||||
mhs.addEdge(pool.Token0, pool.Token1, pool)
|
||||
mhs.addEdge(pool.Token1, pool.Token0, pool)
|
||||
}
|
||||
|
||||
// addEdge adds an edge to the graph
|
||||
func (mhs *MultiHopScanner) addEdge(from, to common.Address, pool *PoolInfo) {
|
||||
if mhs.tokenGraph.adjacencyList[from] == nil {
|
||||
mhs.tokenGraph.adjacencyList[from] = make(map[common.Address][]*PoolInfo)
|
||||
}
|
||||
mhs.tokenGraph.adjacencyList[from][to] = append(mhs.tokenGraph.adjacencyList[from][to], pool)
|
||||
}
|
||||
|
||||
// GetAdjacentTokens returns tokens adjacent to the given token
|
||||
func (tg *TokenGraph) GetAdjacentTokens(token common.Address) map[common.Address][]*PoolInfo {
|
||||
tg.mutex.RLock()
|
||||
defer tg.mutex.RUnlock()
|
||||
|
||||
if adjacent, exists := tg.adjacencyList[token]; exists {
|
||||
return adjacent
|
||||
}
|
||||
return make(map[common.Address][]*PoolInfo)
|
||||
}
|
||||
|
||||
// filterProfitablePaths filters paths that meet profitability criteria
|
||||
func (mhs *MultiHopScanner) filterProfitablePaths(paths []*ArbitragePath) []*ArbitragePath {
|
||||
var profitable []*ArbitragePath
|
||||
|
||||
for _, path := range paths {
|
||||
if mhs.isProfitable(path) {
|
||||
profitable = append(profitable, path)
|
||||
}
|
||||
}
|
||||
|
||||
return profitable
|
||||
}
|
||||
|
||||
// isProfitable checks if a path meets profitability criteria
|
||||
func (mhs *MultiHopScanner) isProfitable(path *ArbitragePath) bool {
|
||||
// Check minimum profit threshold
|
||||
if path.NetProfit.Cmp(mhs.minProfitWei) < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check ROI threshold (minimum 1%)
|
||||
if path.ROI < 1.0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isPoolUsable checks if a pool has sufficient liquidity and is recent
|
||||
func (mhs *MultiHopScanner) isPoolUsable(pool *PoolInfo) bool {
|
||||
// Check if pool data is recent (within 5 minutes)
|
||||
if time.Since(pool.LastUpdated) > 5*time.Minute {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check minimum liquidity (equivalent to 0.1 ETH)
|
||||
minLiquidity := uint256.NewInt(100000000000000000)
|
||||
if pool.Liquidity.Cmp(minLiquidity) < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// estimateHopGasCost estimates gas cost for a single hop
|
||||
func (mhs *MultiHopScanner) estimateHopGasCost(protocol string) *big.Int {
|
||||
switch protocol {
|
||||
case "UniswapV3":
|
||||
return big.NewInt(150000) // ~150k gas per V3 swap
|
||||
case "UniswapV2":
|
||||
return big.NewInt(120000) // ~120k gas per V2 swap
|
||||
case "SushiSwap":
|
||||
return big.NewInt(120000) // Similar to V2
|
||||
default:
|
||||
return big.NewInt(150000) // Conservative estimate
|
||||
}
|
||||
}
|
||||
|
||||
// getCachedPaths retrieves cached paths
|
||||
func (mhs *MultiHopScanner) getCachedPaths(key string) []*ArbitragePath {
|
||||
mhs.cacheMutex.RLock()
|
||||
defer mhs.cacheMutex.RUnlock()
|
||||
|
||||
if paths, exists := mhs.pathCache[key]; exists {
|
||||
// Check if cache is still valid
|
||||
if len(paths) > 0 && time.Since(paths[0].LastUpdated) < mhs.cacheExpiry {
|
||||
return paths
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setCachedPaths stores paths in cache
|
||||
func (mhs *MultiHopScanner) setCachedPaths(key string, paths []*ArbitragePath) {
|
||||
mhs.cacheMutex.Lock()
|
||||
defer mhs.cacheMutex.Unlock()
|
||||
|
||||
mhs.pathCache[key] = paths
|
||||
}
|
||||
403
pkg/arbitrage/multihop_test.go
Normal file
403
pkg/arbitrage/multihop_test.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/market"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockMarketManager is a mock implementation of MarketManager for testing
|
||||
type MockMarketManager struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockMarketManager) GetAllPools() []market.PoolData {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]market.PoolData)
|
||||
}
|
||||
|
||||
func (m *MockMarketManager) GetPool(ctx context.Context, poolAddress common.Address) (*market.PoolData, error) {
|
||||
args := m.Called(ctx, poolAddress)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*market.PoolData), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMarketManager) GetPoolsByTokens(token0, token1 common.Address) []*market.PoolData {
|
||||
args := m.Called(token0, token1)
|
||||
return args.Get(0).([]*market.PoolData)
|
||||
}
|
||||
|
||||
func (m *MockMarketManager) UpdatePool(poolAddress common.Address, liquidity *uint256.Int, sqrtPriceX96 *uint256.Int, tick int) {
|
||||
m.Called(poolAddress, liquidity, sqrtPriceX96, tick)
|
||||
}
|
||||
|
||||
func (m *MockMarketManager) GetPoolsByTokensWithProtocol(token0, token1 common.Address, protocol string) []*market.PoolData {
|
||||
args := m.Called(token0, token1, protocol)
|
||||
return args.Get(0).([]*market.PoolData)
|
||||
}
|
||||
|
||||
// TestNewMultiHopScanner tests the creation of a new MultiHopScanner
|
||||
func TestNewMultiHopScanner(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
|
||||
scanner := NewMultiHopScanner(log, marketMgr)
|
||||
|
||||
assert.NotNil(t, scanner)
|
||||
assert.Equal(t, log, scanner.logger)
|
||||
assert.Equal(t, marketMgr, scanner.marketMgr)
|
||||
assert.Equal(t, 4, scanner.maxHops)
|
||||
assert.Equal(t, "1000000000000000", scanner.minProfitWei.String())
|
||||
assert.Equal(t, 0.03, scanner.maxSlippage)
|
||||
assert.Equal(t, 100, scanner.maxPaths)
|
||||
assert.Equal(t, time.Millisecond*500, scanner.pathTimeout)
|
||||
assert.NotNil(t, scanner.pathCache)
|
||||
assert.NotNil(t, scanner.tokenGraph)
|
||||
assert.NotNil(t, scanner.pools)
|
||||
}
|
||||
|
||||
// TestTokenGraph tests the TokenGraph functionality
|
||||
func TestTokenGraph(t *testing.T) {
|
||||
graph := NewTokenGraph()
|
||||
assert.NotNil(t, graph)
|
||||
assert.NotNil(t, graph.adjacencyList)
|
||||
|
||||
// Test adding edges
|
||||
tokenA := common.HexToAddress("0xA")
|
||||
tokenB := common.HexToAddress("0xB")
|
||||
pool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: tokenA,
|
||||
Token1: tokenB,
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
// Add pool to graph
|
||||
graph.mutex.Lock()
|
||||
graph.adjacencyList[tokenA] = make(map[common.Address][]*PoolInfo)
|
||||
graph.adjacencyList[tokenA][tokenB] = append(graph.adjacencyList[tokenA][tokenB], pool)
|
||||
graph.mutex.Unlock()
|
||||
|
||||
// Test getting adjacent tokens
|
||||
adjacent := graph.GetAdjacentTokens(tokenA)
|
||||
assert.Len(t, adjacent, 1)
|
||||
assert.Contains(t, adjacent, tokenB)
|
||||
assert.Len(t, adjacent[tokenB], 1)
|
||||
assert.Equal(t, pool, adjacent[tokenB][0])
|
||||
}
|
||||
|
||||
// TestIsPoolUsable tests the isPoolUsable function
|
||||
func TestIsPoolUsable(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
scanner := NewMultiHopScanner(log, marketMgr)
|
||||
|
||||
// Test usable pool (recent and sufficient liquidity)
|
||||
now := time.Now()
|
||||
usablePool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000), // 1 ETH worth of liquidity
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: now,
|
||||
}
|
||||
|
||||
assert.True(t, scanner.isPoolUsable(usablePool))
|
||||
|
||||
// Test pool with insufficient liquidity
|
||||
unusablePool1 := &PoolInfo{
|
||||
Address: common.HexToAddress("0x2"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(10000000000000000), // 0.01 ETH worth of liquidity (too little)
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: now,
|
||||
}
|
||||
|
||||
assert.False(t, scanner.isPoolUsable(unusablePool1))
|
||||
|
||||
// Test stale pool
|
||||
stalePool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x3"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: now.Add(-10 * time.Minute), // 10 minutes ago (stale)
|
||||
}
|
||||
|
||||
assert.False(t, scanner.isPoolUsable(stalePool))
|
||||
}
|
||||
|
||||
// TestCalculateSimpleAMMOutput tests the calculateSimpleAMMOutput function
|
||||
func TestCalculateSimpleAMMOutput(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
scanner := NewMultiHopScanner(log, marketMgr)
|
||||
|
||||
// Create a pool with known values for testing
|
||||
tokenIn := common.HexToAddress("0xA")
|
||||
tokenOut := common.HexToAddress("0xB")
|
||||
|
||||
// Create a pool with realistic values
|
||||
// SqrtPriceX96 = 79228162514264337593543950336 (represents 1.0 price)
|
||||
// Liquidity = 1000000000000000000 (1 ETH)
|
||||
pool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: tokenIn,
|
||||
Token1: tokenOut,
|
||||
Protocol: "UniswapV2",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
// Test with 1 ETH input
|
||||
amountIn := big.NewInt(1000000000000000000) // 1 ETH
|
||||
|
||||
output, err := scanner.calculateSimpleAMMOutput(amountIn, pool, tokenIn, tokenOut)
|
||||
|
||||
// We should get a valid output
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, output)
|
||||
assert.True(t, output.Sign() > 0)
|
||||
|
||||
// Test with missing data
|
||||
badPool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x2"),
|
||||
Token0: tokenIn,
|
||||
Token1: tokenOut,
|
||||
Protocol: "UniswapV2",
|
||||
Fee: 3000,
|
||||
Liquidity: nil, // Missing liquidity
|
||||
SqrtPriceX96: nil, // Missing sqrtPriceX96
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
output, err = scanner.calculateSimpleAMMOutput(amountIn, badPool, tokenIn, tokenOut)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, output)
|
||||
}
|
||||
|
||||
// TestCalculateUniswapV3Output tests the calculateUniswapV3Output function
|
||||
func TestCalculateUniswapV3Output(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
scanner := NewMultiHopScanner(log, marketMgr)
|
||||
|
||||
// Create a pool with known values for testing
|
||||
tokenIn := common.HexToAddress("0xA")
|
||||
tokenOut := common.HexToAddress("0xB")
|
||||
|
||||
// Create a pool with realistic values
|
||||
pool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: tokenIn,
|
||||
Token1: tokenOut,
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
// Test with 1 ETH input
|
||||
amountIn := big.NewInt(1000000000000000000) // 1 ETH
|
||||
|
||||
output, err := scanner.calculateUniswapV3Output(amountIn, pool, tokenIn, tokenOut)
|
||||
|
||||
// We should get a valid output
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, output)
|
||||
assert.True(t, output.Sign() > 0)
|
||||
}
|
||||
|
||||
// TestEstimateHopGasCost tests the estimateHopGasCost function
|
||||
func TestEstimateHopGasCost(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
scanner := NewMultiHopScanner(log, marketMgr)
|
||||
|
||||
// Test UniswapV3
|
||||
gas := scanner.estimateHopGasCost("UniswapV3")
|
||||
assert.Equal(t, int64(150000), gas.Int64())
|
||||
|
||||
// Test UniswapV2
|
||||
gas = scanner.estimateHopGasCost("UniswapV2")
|
||||
assert.Equal(t, int64(120000), gas.Int64())
|
||||
|
||||
// Test SushiSwap
|
||||
gas = scanner.estimateHopGasCost("SushiSwap")
|
||||
assert.Equal(t, int64(120000), gas.Int64())
|
||||
|
||||
// Test default case
|
||||
gas = scanner.estimateHopGasCost("UnknownProtocol")
|
||||
assert.Equal(t, int64(150000), gas.Int64())
|
||||
}
|
||||
|
||||
// TestIsProfitable tests the isProfitable function
|
||||
func TestIsProfitable(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
scanner := NewMultiHopScanner(log, marketMgr)
|
||||
|
||||
// Create a profitable path
|
||||
profitablePath := &ArbitragePath{
|
||||
NetProfit: big.NewInt(2000000000000000000), // 2 ETH profit
|
||||
ROI: 5.0, // 5% ROI
|
||||
}
|
||||
|
||||
assert.True(t, scanner.isProfitable(profitablePath))
|
||||
|
||||
// Create an unprofitable path (below minimum profit)
|
||||
unprofitablePath1 := &ArbitragePath{
|
||||
NetProfit: big.NewInt(100000000000000000), // 0.1 ETH profit (below 0.001 ETH threshold)
|
||||
ROI: 0.5, // 0.5% ROI
|
||||
}
|
||||
|
||||
assert.False(t, scanner.isProfitable(unprofitablePath1))
|
||||
|
||||
// Create a path with good profit but poor ROI
|
||||
unprofitablePath2 := &ArbitragePath{
|
||||
NetProfit: big.NewInt(5000000000000000000), // 5 ETH profit
|
||||
ROI: 0.5, // 0.5% ROI (below 1% threshold)
|
||||
}
|
||||
|
||||
assert.False(t, scanner.isProfitable(unprofitablePath2))
|
||||
}
|
||||
|
||||
// TestCreateArbitragePath tests the createArbitragePath function
|
||||
func TestCreateArbitragePath(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
scanner := NewMultiHopScanner(log, marketMgr)
|
||||
|
||||
// Test with invalid inputs
|
||||
tokens := []common.Address{
|
||||
common.HexToAddress("0xA"),
|
||||
common.HexToAddress("0xB"),
|
||||
}
|
||||
pools := []*PoolInfo{
|
||||
{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
initialAmount := big.NewInt(1000000000000000000) // 1 ETH
|
||||
|
||||
// This should fail because we need at least 3 tokens for a valid arbitrage path (A->B->A)
|
||||
path := scanner.createArbitragePath(tokens, pools, initialAmount)
|
||||
assert.Nil(t, path)
|
||||
|
||||
// Test with valid inputs (triangle: A->B->C->A)
|
||||
validTokens := []common.Address{
|
||||
common.HexToAddress("0xA"),
|
||||
common.HexToAddress("0xB"),
|
||||
common.HexToAddress("0xC"),
|
||||
common.HexToAddress("0xA"), // Back to start
|
||||
}
|
||||
validPools := []*PoolInfo{
|
||||
{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("0x2"),
|
||||
Token0: common.HexToAddress("0xB"),
|
||||
Token1: common.HexToAddress("0xC"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("0x3"),
|
||||
Token0: common.HexToAddress("0xC"),
|
||||
Token1: common.HexToAddress("0xA"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
path = scanner.createArbitragePath(validTokens, validPools, initialAmount)
|
||||
assert.NotNil(t, path)
|
||||
assert.Len(t, path.Tokens, 4)
|
||||
assert.Len(t, path.Pools, 3)
|
||||
assert.Len(t, path.Protocols, 3)
|
||||
assert.Len(t, path.Fees, 3)
|
||||
assert.NotNil(t, path.EstimatedGas)
|
||||
assert.NotNil(t, path.NetProfit)
|
||||
}
|
||||
|
||||
// TestScanForArbitrage tests the main ScanForArbitrage function
|
||||
func TestScanForArbitrage(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
|
||||
// Create a mock market manager
|
||||
mockMarketMgr := &MockMarketManager{}
|
||||
|
||||
// Set up mock expectations
|
||||
mockMarketMgr.On("GetAllPools").Return([]market.PoolData{
|
||||
{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: uint256.NewInt(79228162514264337593543950336),
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
})
|
||||
|
||||
scanner := NewMultiHopScanner(log, mockMarketMgr)
|
||||
|
||||
ctx := context.Background()
|
||||
triggerToken := common.HexToAddress("0xA")
|
||||
amount := big.NewInt(1000000000000000000) // 1 ETH
|
||||
|
||||
paths, err := scanner.ScanForArbitrage(ctx, triggerToken, amount)
|
||||
|
||||
// For now, we expect it to return without error, even if no profitable paths are found
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, paths)
|
||||
// It's okay to return an empty slice if no profitable paths are found
|
||||
}
|
||||
686
pkg/arbitrage/service_old.go.bak
Normal file
686
pkg/arbitrage/service_old.go.bak
Normal file
@@ -0,0 +1,686 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/fraktal/mev-beta/internal/config"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/security"
|
||||
)
|
||||
|
||||
// ArbitrageService orchestrates arbitrage detection and execution
|
||||
type ArbitrageService struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
config *config.ArbitrageConfig
|
||||
|
||||
// Core components
|
||||
marketManager *market.MarketManager
|
||||
multiHopScanner *MultiHopScanner
|
||||
executor *ArbitrageExecutor
|
||||
eventMonitor *monitor.Monitor
|
||||
poolDiscovery *pools.PoolDiscovery
|
||||
|
||||
// Channels for communication
|
||||
eventsChan chan *events.SwapEvent
|
||||
arbitrageChan chan *ArbitrageOpportunity
|
||||
resultsChan chan *ExecutionResult
|
||||
|
||||
// State management
|
||||
isRunning bool
|
||||
runMutex sync.RWMutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// Metrics and monitoring
|
||||
stats *ArbitrageStats
|
||||
statsMutex sync.RWMutex
|
||||
|
||||
// Risk management
|
||||
maxConcurrentExecutions int
|
||||
executionSemaphore chan struct{}
|
||||
|
||||
// Database integration
|
||||
database ArbitrageDatabase
|
||||
}
|
||||
|
||||
// ArbitrageOpportunity represents a detected arbitrage opportunity
|
||||
type ArbitrageOpportunity struct {
|
||||
ID string
|
||||
Path *ArbitragePath
|
||||
TriggerEvent *events.SwapEvent
|
||||
DetectedAt time.Time
|
||||
EstimatedProfit *big.Int
|
||||
RequiredAmount *big.Int
|
||||
Urgency int // 1-10 priority level
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// ArbitrageStats contains service statistics
|
||||
type ArbitrageStats struct {
|
||||
TotalOpportunitiesDetected int64
|
||||
TotalOpportunitiesExecuted int64
|
||||
TotalSuccessfulExecutions int64
|
||||
TotalProfitRealized *big.Int
|
||||
TotalGasSpent *big.Int
|
||||
AverageExecutionTime time.Duration
|
||||
LastExecutionTime time.Time
|
||||
}
|
||||
|
||||
// ArbitrageDatabase interface for persistence
|
||||
type ArbitrageDatabase interface {
|
||||
SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error
|
||||
SaveExecution(ctx context.Context, result *ExecutionResult) error
|
||||
GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error)
|
||||
SavePoolData(ctx context.Context, poolData *pools.PoolData) error
|
||||
GetPoolData(ctx context.Context, poolAddress common.Address) (*pools.PoolData, error)
|
||||
}
|
||||
|
||||
// NewArbitrageService creates a new arbitrage service
|
||||
func NewArbitrageService(
|
||||
client *ethclient.Client,
|
||||
logger *logger.Logger,
|
||||
config *config.ArbitrageConfig,
|
||||
keyManager *security.KeyManager,
|
||||
database ArbitrageDatabase,
|
||||
) (*ArbitrageService, error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Create market manager
|
||||
marketManager := market.NewMarketManager(logger, client)
|
||||
|
||||
// Create multi-hop scanner
|
||||
multiHopScanner := NewMultiHopScanner(logger, marketManager)
|
||||
|
||||
// Create arbitrage executor
|
||||
executor, err := NewArbitrageExecutor(
|
||||
client,
|
||||
logger,
|
||||
keyManager,
|
||||
common.HexToAddress(config.ArbitrageContractAddress),
|
||||
common.HexToAddress(config.FlashSwapContractAddress),
|
||||
)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to create arbitrage executor: %w", err)
|
||||
}
|
||||
|
||||
// Create event monitor
|
||||
eventMonitor := monitor.NewMonitor(logger, client)
|
||||
|
||||
// Create pool discovery service
|
||||
poolDiscovery, err := pools.NewPoolDiscovery(client, logger, config.PoolDiscoveryConfig)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to create pool discovery: %w", err)
|
||||
}
|
||||
|
||||
// Initialize stats
|
||||
stats := &ArbitrageStats{
|
||||
TotalProfitRealized: big.NewInt(0),
|
||||
TotalGasSpent: big.NewInt(0),
|
||||
}
|
||||
|
||||
service := &ArbitrageService{
|
||||
client: client,
|
||||
logger: logger,
|
||||
config: config,
|
||||
marketManager: marketManager,
|
||||
multiHopScanner: multiHopScanner,
|
||||
executor: executor,
|
||||
eventMonitor: eventMonitor,
|
||||
poolDiscovery: poolDiscovery,
|
||||
eventsChan: make(chan *events.SwapEvent, 1000),
|
||||
arbitrageChan: make(chan *ArbitrageOpportunity, 100),
|
||||
resultsChan: make(chan *ExecutionResult, 100),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
stats: stats,
|
||||
maxConcurrentExecutions: config.MaxConcurrentExecutions,
|
||||
executionSemaphore: make(chan struct{}, config.MaxConcurrentExecutions),
|
||||
database: database,
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// Start begins the arbitrage service
|
||||
func (as *ArbitrageService) Start() error {
|
||||
as.runMutex.Lock()
|
||||
defer as.runMutex.Unlock()
|
||||
|
||||
if as.isRunning {
|
||||
return fmt.Errorf("arbitrage service is already running")
|
||||
}
|
||||
|
||||
as.logger.Info("Starting arbitrage service...")
|
||||
|
||||
// Start pool discovery
|
||||
if err := as.poolDiscovery.Start(as.ctx); err != nil {
|
||||
return fmt.Errorf("failed to start pool discovery: %w", err)
|
||||
}
|
||||
|
||||
// Start market manager
|
||||
if err := as.marketManager.Start(as.ctx); err != nil {
|
||||
return fmt.Errorf("failed to start market manager: %w", err)
|
||||
}
|
||||
|
||||
// Start event monitor
|
||||
if err := as.eventMonitor.Start(as.ctx); err != nil {
|
||||
return fmt.Errorf("failed to start event monitor: %w", err)
|
||||
}
|
||||
|
||||
// Start worker goroutines
|
||||
go as.eventProcessor()
|
||||
go as.arbitrageDetector()
|
||||
go as.arbitrageExecutor()
|
||||
go as.resultProcessor()
|
||||
go as.poolDataProcessor()
|
||||
go as.statsUpdater()
|
||||
|
||||
as.isRunning = true
|
||||
as.logger.Info("Arbitrage service started successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the arbitrage service
|
||||
func (as *ArbitrageService) Stop() error {
|
||||
as.runMutex.Lock()
|
||||
defer as.runMutex.Unlock()
|
||||
|
||||
if !as.isRunning {
|
||||
return nil
|
||||
}
|
||||
|
||||
as.logger.Info("Stopping arbitrage service...")
|
||||
|
||||
// Cancel context to stop all workers
|
||||
as.cancel()
|
||||
|
||||
// Stop components
|
||||
as.eventMonitor.Stop()
|
||||
as.marketManager.Stop()
|
||||
as.poolDiscovery.Stop()
|
||||
|
||||
// Close channels
|
||||
close(as.eventsChan)
|
||||
close(as.arbitrageChan)
|
||||
close(as.resultsChan)
|
||||
|
||||
as.isRunning = false
|
||||
as.logger.Info("Arbitrage service stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventProcessor processes incoming swap events
|
||||
func (as *ArbitrageService) eventProcessor() {
|
||||
defer as.logger.Info("Event processor stopped")
|
||||
|
||||
// Subscribe to swap events from the event monitor
|
||||
swapEvents := as.eventMonitor.SubscribeToSwapEvents()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
case event := <-swapEvents:
|
||||
if event != nil {
|
||||
as.processSwapEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processSwapEvent processes a single swap event for arbitrage opportunities
|
||||
func (as *ArbitrageService) processSwapEvent(event *events.SwapEvent) {
|
||||
as.logger.Debug(fmt.Sprintf("Processing swap event: token0=%s, token1=%s, amount0=%s, amount1=%s",
|
||||
event.Token0.Hex(), event.Token1.Hex(), event.Amount0.String(), event.Amount1.String()))
|
||||
|
||||
// Check if this swap is large enough to potentially move prices
|
||||
if !as.isSignificantSwap(event) {
|
||||
return
|
||||
}
|
||||
|
||||
// Forward to arbitrage detection
|
||||
select {
|
||||
case as.eventsChan <- event:
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
default:
|
||||
as.logger.Warn("Event channel full, dropping swap event")
|
||||
}
|
||||
}
|
||||
|
||||
// isSignificantSwap checks if a swap is large enough to create arbitrage opportunities
|
||||
func (as *ArbitrageService) isSignificantSwap(event *events.SwapEvent) bool {
|
||||
// Convert amounts to absolute values for comparison
|
||||
amount0Abs := new(big.Int).Abs(event.Amount0)
|
||||
amount1Abs := new(big.Int).Abs(event.Amount1)
|
||||
|
||||
// Check if either amount is above our threshold
|
||||
minSwapSize := big.NewInt(as.config.MinSignificantSwapSize)
|
||||
|
||||
return amount0Abs.Cmp(minSwapSize) > 0 || amount1Abs.Cmp(minSwapSize) > 0
|
||||
}
|
||||
|
||||
// arbitrageDetector detects arbitrage opportunities from swap events
|
||||
func (as *ArbitrageService) arbitrageDetector() {
|
||||
defer as.logger.Info("Arbitrage detector stopped")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
case event := <-as.eventsChan:
|
||||
if event != nil {
|
||||
as.detectArbitrageOpportunities(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detectArbitrageOpportunities scans for arbitrage opportunities triggered by an event
|
||||
func (as *ArbitrageService) detectArbitrageOpportunities(event *events.SwapEvent) {
|
||||
start := time.Now()
|
||||
|
||||
// Determine the tokens involved in potential arbitrage
|
||||
tokens := []common.Address{event.Token0, event.Token1}
|
||||
|
||||
var allOpportunities []*ArbitrageOpportunity
|
||||
|
||||
// Scan for opportunities starting with each token
|
||||
for _, token := range tokens {
|
||||
// Determine appropriate amount to use for scanning
|
||||
scanAmount := as.calculateScanAmount(event, token)
|
||||
|
||||
// Use multi-hop scanner to find arbitrage paths
|
||||
paths, err := as.multiHopScanner.ScanForArbitrage(as.ctx, token, scanAmount)
|
||||
if err != nil {
|
||||
as.logger.Debug(fmt.Sprintf("Arbitrage scan failed for token %s: %v", token.Hex(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert paths to opportunities
|
||||
for _, path := range paths {
|
||||
if as.isValidOpportunity(path) {
|
||||
opportunity := &ArbitrageOpportunity{
|
||||
ID: as.generateOpportunityID(path, event),
|
||||
Path: path,
|
||||
TriggerEvent: event,
|
||||
DetectedAt: time.Now(),
|
||||
EstimatedProfit: path.NetProfit,
|
||||
RequiredAmount: scanAmount,
|
||||
Urgency: as.calculateUrgency(path),
|
||||
ExpiresAt: time.Now().Add(as.config.OpportunityTTL),
|
||||
}
|
||||
allOpportunities = append(allOpportunities, opportunity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort opportunities by urgency and profit
|
||||
as.rankOpportunities(allOpportunities)
|
||||
|
||||
// Send top opportunities for execution
|
||||
maxOpportunities := as.config.MaxOpportunitiesPerEvent
|
||||
for i, opportunity := range allOpportunities {
|
||||
if i >= maxOpportunities {
|
||||
break
|
||||
}
|
||||
|
||||
// Update stats
|
||||
as.statsMutex.Lock()
|
||||
as.stats.TotalOpportunitiesDetected++
|
||||
as.statsMutex.Unlock()
|
||||
|
||||
// Save to database
|
||||
if err := as.database.SaveOpportunity(as.ctx, opportunity); err != nil {
|
||||
as.logger.Warn(fmt.Sprintf("Failed to save opportunity to database: %v", err))
|
||||
}
|
||||
|
||||
// Send for execution
|
||||
select {
|
||||
case as.arbitrageChan <- opportunity:
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
default:
|
||||
as.logger.Warn("Arbitrage channel full, dropping opportunity")
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
as.logger.Debug(fmt.Sprintf("Arbitrage detection completed in %v: found %d opportunities",
|
||||
elapsed, len(allOpportunities)))
|
||||
}
|
||||
|
||||
// isValidOpportunity validates an arbitrage opportunity
|
||||
func (as *ArbitrageService) isValidOpportunity(path *ArbitragePath) bool {
|
||||
// Check minimum profit threshold
|
||||
minProfit := big.NewInt(as.config.MinProfitWei)
|
||||
if path.NetProfit.Cmp(minProfit) < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check minimum ROI
|
||||
if path.ROI < as.config.MinROIPercent {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check path freshness
|
||||
if time.Since(path.LastUpdated) > as.config.MaxPathAge {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check gas profitability
|
||||
currentGasPrice, err := as.client.SuggestGasPrice(as.ctx)
|
||||
if err != nil {
|
||||
// Assume worst case if we can't get gas price
|
||||
currentGasPrice = big.NewInt(as.config.MaxGasPriceWei)
|
||||
}
|
||||
|
||||
return as.executor.IsProfitableAfterGas(path, currentGasPrice)
|
||||
}
|
||||
|
||||
// calculateScanAmount determines the optimal amount to use for arbitrage scanning
|
||||
func (as *ArbitrageService) calculateScanAmount(event *events.SwapEvent, token common.Address) *big.Int {
|
||||
// Base amount on the swap size, but cap it for risk management
|
||||
var swapAmount *big.Int
|
||||
|
||||
if token == event.Token0 {
|
||||
swapAmount = new(big.Int).Abs(event.Amount0)
|
||||
} else {
|
||||
swapAmount = new(big.Int).Abs(event.Amount1)
|
||||
}
|
||||
|
||||
// Use a fraction of the swap amount (default 10%)
|
||||
scanAmount := new(big.Int).Div(swapAmount, big.NewInt(10))
|
||||
|
||||
// Ensure minimum scan amount
|
||||
minAmount := big.NewInt(as.config.MinScanAmountWei)
|
||||
if scanAmount.Cmp(minAmount) < 0 {
|
||||
scanAmount = minAmount
|
||||
}
|
||||
|
||||
// Cap maximum scan amount
|
||||
maxAmount := big.NewInt(as.config.MaxScanAmountWei)
|
||||
if scanAmount.Cmp(maxAmount) > 0 {
|
||||
scanAmount = maxAmount
|
||||
}
|
||||
|
||||
return scanAmount
|
||||
}
|
||||
|
||||
// calculateUrgency calculates the urgency level of an opportunity
|
||||
func (as *ArbitrageService) calculateUrgency(path *ArbitragePath) int {
|
||||
// Base urgency on ROI
|
||||
urgency := int(path.ROI / 2) // 2% ROI = urgency level 1
|
||||
|
||||
// Boost urgency for higher profits
|
||||
profitETH := new(big.Float).SetInt(path.NetProfit)
|
||||
profitETH.Quo(profitETH, big.NewFloat(1e18))
|
||||
profitFloat, _ := profitETH.Float64()
|
||||
|
||||
if profitFloat > 1.0 { // > 1 ETH profit
|
||||
urgency += 5
|
||||
} else if profitFloat > 0.1 { // > 0.1 ETH profit
|
||||
urgency += 2
|
||||
}
|
||||
|
||||
// Cap urgency between 1 and 10
|
||||
if urgency < 1 {
|
||||
urgency = 1
|
||||
}
|
||||
if urgency > 10 {
|
||||
urgency = 10
|
||||
}
|
||||
|
||||
return urgency
|
||||
}
|
||||
|
||||
// rankOpportunities sorts opportunities by priority
|
||||
func (as *ArbitrageService) rankOpportunities(opportunities []*ArbitrageOpportunity) {
|
||||
// Sort by urgency (descending) then by profit (descending)
|
||||
for i := 0; i < len(opportunities); i++ {
|
||||
for j := i + 1; j < len(opportunities); j++ {
|
||||
iOpp := opportunities[i]
|
||||
jOpp := opportunities[j]
|
||||
|
||||
if jOpp.Urgency > iOpp.Urgency {
|
||||
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
|
||||
} else if jOpp.Urgency == iOpp.Urgency {
|
||||
if jOpp.EstimatedProfit.Cmp(iOpp.EstimatedProfit) > 0 {
|
||||
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// arbitrageExecutor executes arbitrage opportunities
|
||||
func (as *ArbitrageService) arbitrageExecutor() {
|
||||
defer as.logger.Info("Arbitrage executor stopped")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
case opportunity := <-as.arbitrageChan:
|
||||
if opportunity != nil {
|
||||
// Rate limit concurrent executions
|
||||
select {
|
||||
case as.executionSemaphore <- struct{}{}:
|
||||
go as.executeOpportunity(opportunity)
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
default:
|
||||
as.logger.Warn("Too many concurrent executions, dropping opportunity")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeOpportunity executes a single arbitrage opportunity
|
||||
func (as *ArbitrageService) executeOpportunity(opportunity *ArbitrageOpportunity) {
|
||||
defer func() {
|
||||
<-as.executionSemaphore // Release semaphore
|
||||
}()
|
||||
|
||||
// Check if opportunity is still valid
|
||||
if time.Now().After(opportunity.ExpiresAt) {
|
||||
as.logger.Debug(fmt.Sprintf("Opportunity %s expired", opportunity.ID))
|
||||
return
|
||||
}
|
||||
|
||||
// Update stats
|
||||
as.statsMutex.Lock()
|
||||
as.stats.TotalOpportunitiesExecuted++
|
||||
as.statsMutex.Unlock()
|
||||
|
||||
// Prepare execution parameters
|
||||
params := &ArbitrageParams{
|
||||
Path: opportunity.Path,
|
||||
InputAmount: opportunity.RequiredAmount,
|
||||
MinOutputAmount: as.calculateMinOutput(opportunity),
|
||||
Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()),
|
||||
FlashSwapData: []byte{}, // Additional data if needed
|
||||
}
|
||||
|
||||
as.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH",
|
||||
opportunity.ID, formatEther(opportunity.EstimatedProfit)))
|
||||
|
||||
// Execute the arbitrage
|
||||
result, err := as.executor.ExecuteArbitrage(as.ctx, params)
|
||||
if err != nil {
|
||||
as.logger.Error(fmt.Sprintf("Arbitrage execution failed for opportunity %s: %v",
|
||||
opportunity.ID, err))
|
||||
}
|
||||
|
||||
// Send result for processing
|
||||
select {
|
||||
case as.resultsChan <- result:
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
default:
|
||||
as.logger.Warn("Results channel full, dropping execution result")
|
||||
}
|
||||
}
|
||||
|
||||
// calculateMinOutput calculates minimum acceptable output for slippage protection
|
||||
func (as *ArbitrageService) calculateMinOutput(opportunity *ArbitrageOpportunity) *big.Int {
|
||||
// Calculate expected output
|
||||
expectedOutput := new(big.Int).Add(opportunity.RequiredAmount, opportunity.EstimatedProfit)
|
||||
|
||||
// Apply slippage tolerance
|
||||
slippageTolerance := as.config.SlippageTolerance
|
||||
slippageMultiplier := big.NewFloat(1.0 - slippageTolerance)
|
||||
|
||||
expectedFloat := new(big.Float).SetInt(expectedOutput)
|
||||
minOutputFloat := new(big.Float).Mul(expectedFloat, slippageMultiplier)
|
||||
|
||||
minOutput := new(big.Int)
|
||||
minOutputFloat.Int(minOutput)
|
||||
|
||||
return minOutput
|
||||
}
|
||||
|
||||
// resultProcessor processes execution results
|
||||
func (as *ArbitrageService) resultProcessor() {
|
||||
defer as.logger.Info("Result processor stopped")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
case result := <-as.resultsChan:
|
||||
if result != nil {
|
||||
as.processExecutionResult(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processExecutionResult processes a single execution result
|
||||
func (as *ArbitrageService) processExecutionResult(result *ExecutionResult) {
|
||||
// Update statistics
|
||||
as.statsMutex.Lock()
|
||||
if result.Success {
|
||||
as.stats.TotalSuccessfulExecutions++
|
||||
as.stats.TotalProfitRealized.Add(as.stats.TotalProfitRealized, result.ProfitRealized)
|
||||
}
|
||||
|
||||
gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed)))
|
||||
as.stats.TotalGasSpent.Add(as.stats.TotalGasSpent, gasCost)
|
||||
as.stats.LastExecutionTime = time.Now()
|
||||
as.statsMutex.Unlock()
|
||||
|
||||
// Save to database
|
||||
if err := as.database.SaveExecution(as.ctx, result); err != nil {
|
||||
as.logger.Warn(fmt.Sprintf("Failed to save execution result to database: %v", err))
|
||||
}
|
||||
|
||||
// Log results
|
||||
if result.Success {
|
||||
as.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d",
|
||||
result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed))
|
||||
} else {
|
||||
as.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v",
|
||||
result.TransactionHash.Hex(), result.Error))
|
||||
}
|
||||
}
|
||||
|
||||
// poolDataProcessor handles pool data updates and saves them to database
|
||||
func (as *ArbitrageService) poolDataProcessor() {
|
||||
defer as.logger.Info("Pool data processor stopped")
|
||||
|
||||
// Subscribe to pool discovery events
|
||||
poolEvents := as.poolDiscovery.SubscribeToPoolEvents()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
case poolData := <-poolEvents:
|
||||
if poolData != nil {
|
||||
// Save pool data to database
|
||||
if err := as.database.SavePoolData(as.ctx, poolData); err != nil {
|
||||
as.logger.Warn(fmt.Sprintf("Failed to save pool data: %v", err))
|
||||
}
|
||||
|
||||
// Update market manager with new pool
|
||||
as.marketManager.AddPool(poolData)
|
||||
|
||||
as.logger.Debug(fmt.Sprintf("Added new pool: %s (%s/%s)",
|
||||
poolData.Address.Hex(), poolData.Token0.Hex(), poolData.Token1.Hex()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// statsUpdater periodically logs service statistics
|
||||
func (as *ArbitrageService) statsUpdater() {
|
||||
defer as.logger.Info("Stats updater stopped")
|
||||
|
||||
ticker := time.NewTicker(as.config.StatsUpdateInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-as.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
as.logStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logStats logs current service statistics
|
||||
func (as *ArbitrageService) logStats() {
|
||||
as.statsMutex.RLock()
|
||||
stats := *as.stats
|
||||
as.statsMutex.RUnlock()
|
||||
|
||||
successRate := 0.0
|
||||
if stats.TotalOpportunitiesExecuted > 0 {
|
||||
successRate = float64(stats.TotalSuccessfulExecutions) / float64(stats.TotalOpportunitiesExecuted) * 100
|
||||
}
|
||||
|
||||
as.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Success Rate: %.2f%%, "+
|
||||
"Total Profit: %s ETH, Total Gas: %s ETH",
|
||||
stats.TotalOpportunitiesDetected,
|
||||
stats.TotalOpportunitiesExecuted,
|
||||
successRate,
|
||||
formatEther(stats.TotalProfitRealized),
|
||||
formatEther(stats.TotalGasSpent)))
|
||||
}
|
||||
|
||||
// generateOpportunityID generates a unique ID for an arbitrage opportunity
|
||||
func (as *ArbitrageService) generateOpportunityID(path *ArbitragePath, event *events.SwapEvent) string {
|
||||
return fmt.Sprintf("%s_%s_%d", event.TxHash.Hex()[:10], path.Tokens[0].Hex()[:8], time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// GetStats returns current service statistics
|
||||
func (as *ArbitrageService) GetStats() *ArbitrageStats {
|
||||
as.statsMutex.RLock()
|
||||
defer as.statsMutex.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
statsCopy := *as.stats
|
||||
return &statsCopy
|
||||
}
|
||||
|
||||
// IsRunning returns whether the service is currently running
|
||||
func (as *ArbitrageService) IsRunning() bool {
|
||||
as.runMutex.RLock()
|
||||
defer as.runMutex.RUnlock()
|
||||
return as.isRunning
|
||||
}
|
||||
831
pkg/arbitrage/service_simple.go
Normal file
831
pkg/arbitrage/service_simple.go
Normal file
@@ -0,0 +1,831 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/fraktal/mev-beta/internal/config"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/security"
|
||||
)
|
||||
|
||||
// TokenPair represents the two tokens in a pool
|
||||
type TokenPair struct {
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
}
|
||||
|
||||
// ArbitrageOpportunity represents a detected arbitrage opportunity
|
||||
type ArbitrageOpportunity struct {
|
||||
ID string
|
||||
Path *ArbitragePath
|
||||
TriggerEvent *SimpleSwapEvent
|
||||
DetectedAt time.Time
|
||||
EstimatedProfit *big.Int
|
||||
RequiredAmount *big.Int
|
||||
Urgency int // 1-10 priority level
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// ArbitrageStats contains service statistics
|
||||
type ArbitrageStats struct {
|
||||
TotalOpportunitiesDetected int64
|
||||
TotalOpportunitiesExecuted int64
|
||||
TotalSuccessfulExecutions int64
|
||||
TotalProfitRealized *big.Int
|
||||
TotalGasSpent *big.Int
|
||||
AverageExecutionTime time.Duration
|
||||
LastExecutionTime time.Time
|
||||
}
|
||||
|
||||
// ArbitrageDatabase interface for persistence
|
||||
type ArbitrageDatabase interface {
|
||||
SaveOpportunity(ctx context.Context, opportunity *ArbitrageOpportunity) error
|
||||
SaveExecution(ctx context.Context, result *ExecutionResult) error
|
||||
GetExecutionHistory(ctx context.Context, limit int) ([]*ExecutionResult, error)
|
||||
SavePoolData(ctx context.Context, poolData *SimplePoolData) error
|
||||
GetPoolData(ctx context.Context, poolAddress common.Address) (*SimplePoolData, error)
|
||||
}
|
||||
|
||||
// SimpleArbitrageService is a simplified arbitrage service without circular dependencies
|
||||
type SimpleArbitrageService struct {
|
||||
client *ethclient.Client
|
||||
logger *logger.Logger
|
||||
config *config.ArbitrageConfig
|
||||
|
||||
// Core components
|
||||
multiHopScanner *MultiHopScanner
|
||||
executor *ArbitrageExecutor
|
||||
|
||||
// Token cache for pool addresses
|
||||
tokenCache map[common.Address]TokenPair
|
||||
tokenCacheMutex sync.RWMutex
|
||||
|
||||
// State management
|
||||
isRunning bool
|
||||
runMutex sync.RWMutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// Metrics and monitoring
|
||||
stats *ArbitrageStats
|
||||
statsMutex sync.RWMutex
|
||||
|
||||
// Database integration
|
||||
database ArbitrageDatabase
|
||||
}
|
||||
|
||||
// SimpleSwapEvent represents a swap event for arbitrage detection
|
||||
type SimpleSwapEvent struct {
|
||||
TxHash common.Hash
|
||||
PoolAddress common.Address
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
Amount0 *big.Int
|
||||
Amount1 *big.Int
|
||||
SqrtPriceX96 *big.Int
|
||||
Liquidity *big.Int
|
||||
Tick int32
|
||||
BlockNumber uint64
|
||||
LogIndex uint
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// SimplePoolData represents basic pool information
|
||||
type SimplePoolData struct {
|
||||
Address common.Address
|
||||
Token0 common.Address
|
||||
Token1 common.Address
|
||||
Fee int64
|
||||
Liquidity *big.Int
|
||||
SqrtPriceX96 *big.Int
|
||||
Tick int32
|
||||
BlockNumber uint64
|
||||
TxHash common.Hash
|
||||
LogIndex uint
|
||||
LastUpdated time.Time
|
||||
}
|
||||
|
||||
// NewSimpleArbitrageService creates a new simplified arbitrage service
|
||||
func NewSimpleArbitrageService(
|
||||
client *ethclient.Client,
|
||||
logger *logger.Logger,
|
||||
config *config.ArbitrageConfig,
|
||||
keyManager *security.KeyManager,
|
||||
database ArbitrageDatabase,
|
||||
) (*SimpleArbitrageService, error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Create multi-hop scanner with simple market manager
|
||||
multiHopScanner := NewMultiHopScanner(logger, nil)
|
||||
|
||||
// Create arbitrage executor
|
||||
executor, err := NewArbitrageExecutor(
|
||||
client,
|
||||
logger,
|
||||
keyManager,
|
||||
common.HexToAddress(config.ArbitrageContractAddress),
|
||||
common.HexToAddress(config.FlashSwapContractAddress),
|
||||
)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to create arbitrage executor: %w", err)
|
||||
}
|
||||
|
||||
// Initialize stats
|
||||
stats := &ArbitrageStats{
|
||||
TotalProfitRealized: big.NewInt(0),
|
||||
TotalGasSpent: big.NewInt(0),
|
||||
}
|
||||
|
||||
service := &SimpleArbitrageService{
|
||||
client: client,
|
||||
logger: logger,
|
||||
config: config,
|
||||
multiHopScanner: multiHopScanner,
|
||||
executor: executor,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
stats: stats,
|
||||
database: database,
|
||||
tokenCache: make(map[common.Address]TokenPair),
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// Start begins the simplified arbitrage service
|
||||
func (sas *SimpleArbitrageService) Start() error {
|
||||
sas.runMutex.Lock()
|
||||
defer sas.runMutex.Unlock()
|
||||
|
||||
if sas.isRunning {
|
||||
return fmt.Errorf("arbitrage service is already running")
|
||||
}
|
||||
|
||||
sas.logger.Info("Starting simplified arbitrage service...")
|
||||
|
||||
// Start worker goroutines
|
||||
go sas.statsUpdater()
|
||||
go sas.blockchainMonitor()
|
||||
|
||||
sas.isRunning = true
|
||||
sas.logger.Info("Simplified arbitrage service started successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the arbitrage service
|
||||
func (sas *SimpleArbitrageService) Stop() error {
|
||||
sas.runMutex.Lock()
|
||||
defer sas.runMutex.Unlock()
|
||||
|
||||
if !sas.isRunning {
|
||||
return nil
|
||||
}
|
||||
|
||||
sas.logger.Info("Stopping simplified arbitrage service...")
|
||||
|
||||
// Cancel context to stop all workers
|
||||
sas.cancel()
|
||||
|
||||
sas.isRunning = false
|
||||
sas.logger.Info("Simplified arbitrage service stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessSwapEvent processes a swap event for arbitrage opportunities
|
||||
func (sas *SimpleArbitrageService) ProcessSwapEvent(event *SimpleSwapEvent) error {
|
||||
sas.logger.Debug(fmt.Sprintf("Processing swap event: token0=%s, token1=%s, amount0=%s, amount1=%s",
|
||||
event.Token0.Hex(), event.Token1.Hex(), event.Amount0.String(), event.Amount1.String()))
|
||||
|
||||
// Check if this swap is large enough to potentially move prices
|
||||
if !sas.isSignificantSwap(event) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan for arbitrage opportunities
|
||||
return sas.detectArbitrageOpportunities(event)
|
||||
}
|
||||
|
||||
// isSignificantSwap checks if a swap is large enough to create arbitrage opportunities
|
||||
func (sas *SimpleArbitrageService) isSignificantSwap(event *SimpleSwapEvent) bool {
|
||||
// Convert amounts to absolute values for comparison
|
||||
amount0Abs := new(big.Int).Abs(event.Amount0)
|
||||
amount1Abs := new(big.Int).Abs(event.Amount1)
|
||||
|
||||
// Check if either amount is above our threshold
|
||||
minSwapSize := big.NewInt(sas.config.MinSignificantSwapSize)
|
||||
|
||||
return amount0Abs.Cmp(minSwapSize) > 0 || amount1Abs.Cmp(minSwapSize) > 0
|
||||
}
|
||||
|
||||
// detectArbitrageOpportunities scans for arbitrage opportunities triggered by an event
|
||||
func (sas *SimpleArbitrageService) detectArbitrageOpportunities(event *SimpleSwapEvent) error {
|
||||
start := time.Now()
|
||||
|
||||
// Determine the tokens involved in potential arbitrage
|
||||
tokens := []common.Address{event.Token0, event.Token1}
|
||||
|
||||
var allOpportunities []*ArbitrageOpportunity
|
||||
|
||||
// Scan for opportunities starting with each token
|
||||
for _, token := range tokens {
|
||||
// Determine appropriate amount to use for scanning
|
||||
scanAmount := sas.calculateScanAmount(event, token)
|
||||
|
||||
// Use multi-hop scanner to find arbitrage paths
|
||||
paths, err := sas.multiHopScanner.ScanForArbitrage(sas.ctx, token, scanAmount)
|
||||
if err != nil {
|
||||
sas.logger.Debug(fmt.Sprintf("Arbitrage scan failed for token %s: %v", token.Hex(), err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert paths to opportunities
|
||||
for _, path := range paths {
|
||||
if sas.isValidOpportunity(path) {
|
||||
opportunity := &ArbitrageOpportunity{
|
||||
ID: sas.generateOpportunityID(path, event),
|
||||
Path: path,
|
||||
DetectedAt: time.Now(),
|
||||
EstimatedProfit: path.NetProfit,
|
||||
RequiredAmount: scanAmount,
|
||||
Urgency: sas.calculateUrgency(path),
|
||||
ExpiresAt: time.Now().Add(sas.config.OpportunityTTL),
|
||||
}
|
||||
allOpportunities = append(allOpportunities, opportunity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort opportunities by urgency and profit
|
||||
sas.rankOpportunities(allOpportunities)
|
||||
|
||||
// Process top opportunities
|
||||
maxOpportunities := sas.config.MaxOpportunitiesPerEvent
|
||||
for i, opportunity := range allOpportunities {
|
||||
if i >= maxOpportunities {
|
||||
break
|
||||
}
|
||||
|
||||
// Update stats
|
||||
sas.statsMutex.Lock()
|
||||
sas.stats.TotalOpportunitiesDetected++
|
||||
sas.statsMutex.Unlock()
|
||||
|
||||
// Save to database
|
||||
if err := sas.database.SaveOpportunity(sas.ctx, opportunity); err != nil {
|
||||
sas.logger.Warn(fmt.Sprintf("Failed to save opportunity to database: %v", err))
|
||||
}
|
||||
|
||||
// Execute if execution is enabled
|
||||
if sas.config.MaxConcurrentExecutions > 0 {
|
||||
go sas.executeOpportunity(opportunity)
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
sas.logger.Debug(fmt.Sprintf("Arbitrage detection completed in %v: found %d opportunities",
|
||||
elapsed, len(allOpportunities)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeOpportunity executes a single arbitrage opportunity
|
||||
func (sas *SimpleArbitrageService) executeOpportunity(opportunity *ArbitrageOpportunity) {
|
||||
// Check if opportunity is still valid
|
||||
if time.Now().After(opportunity.ExpiresAt) {
|
||||
sas.logger.Debug(fmt.Sprintf("Opportunity %s expired", opportunity.ID))
|
||||
return
|
||||
}
|
||||
|
||||
// Update stats
|
||||
sas.statsMutex.Lock()
|
||||
sas.stats.TotalOpportunitiesExecuted++
|
||||
sas.statsMutex.Unlock()
|
||||
|
||||
// Prepare execution parameters
|
||||
params := &ArbitrageParams{
|
||||
Path: opportunity.Path,
|
||||
InputAmount: opportunity.RequiredAmount,
|
||||
MinOutputAmount: sas.calculateMinOutput(opportunity),
|
||||
Deadline: big.NewInt(time.Now().Add(5 * time.Minute).Unix()),
|
||||
FlashSwapData: []byte{}, // Additional data if needed
|
||||
}
|
||||
|
||||
sas.logger.Info(fmt.Sprintf("Executing arbitrage opportunity %s with estimated profit %s ETH",
|
||||
opportunity.ID, formatEther(opportunity.EstimatedProfit)))
|
||||
|
||||
// Execute the arbitrage
|
||||
result, err := sas.executor.ExecuteArbitrage(sas.ctx, params)
|
||||
if err != nil {
|
||||
sas.logger.Error(fmt.Sprintf("Arbitrage execution failed for opportunity %s: %v",
|
||||
opportunity.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Process execution results
|
||||
sas.processExecutionResult(result)
|
||||
}
|
||||
|
||||
// Helper methods from the original service
|
||||
func (sas *SimpleArbitrageService) isValidOpportunity(path *ArbitragePath) bool {
|
||||
minProfit := big.NewInt(sas.config.MinProfitWei)
|
||||
if path.NetProfit.Cmp(minProfit) < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if path.ROI < sas.config.MinROIPercent {
|
||||
return false
|
||||
}
|
||||
|
||||
if time.Since(path.LastUpdated) > sas.config.MaxPathAge {
|
||||
return false
|
||||
}
|
||||
|
||||
currentGasPrice, err := sas.client.SuggestGasPrice(sas.ctx)
|
||||
if err != nil {
|
||||
currentGasPrice = big.NewInt(sas.config.MaxGasPriceWei)
|
||||
}
|
||||
|
||||
return sas.executor.IsProfitableAfterGas(path, currentGasPrice)
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) calculateScanAmount(event *SimpleSwapEvent, token common.Address) *big.Int {
|
||||
var swapAmount *big.Int
|
||||
|
||||
if token == event.Token0 {
|
||||
swapAmount = new(big.Int).Abs(event.Amount0)
|
||||
} else {
|
||||
swapAmount = new(big.Int).Abs(event.Amount1)
|
||||
}
|
||||
|
||||
scanAmount := new(big.Int).Div(swapAmount, big.NewInt(10))
|
||||
|
||||
minAmount := big.NewInt(sas.config.MinScanAmountWei)
|
||||
if scanAmount.Cmp(minAmount) < 0 {
|
||||
scanAmount = minAmount
|
||||
}
|
||||
|
||||
maxAmount := big.NewInt(sas.config.MaxScanAmountWei)
|
||||
if scanAmount.Cmp(maxAmount) > 0 {
|
||||
scanAmount = maxAmount
|
||||
}
|
||||
|
||||
return scanAmount
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) calculateUrgency(path *ArbitragePath) int {
|
||||
urgency := int(path.ROI / 2)
|
||||
|
||||
profitETH := new(big.Float).SetInt(path.NetProfit)
|
||||
profitETH.Quo(profitETH, big.NewFloat(1e18))
|
||||
profitFloat, _ := profitETH.Float64()
|
||||
|
||||
if profitFloat > 1.0 {
|
||||
urgency += 5
|
||||
} else if profitFloat > 0.1 {
|
||||
urgency += 2
|
||||
}
|
||||
|
||||
if urgency < 1 {
|
||||
urgency = 1
|
||||
}
|
||||
if urgency > 10 {
|
||||
urgency = 10
|
||||
}
|
||||
|
||||
return urgency
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) rankOpportunities(opportunities []*ArbitrageOpportunity) {
|
||||
for i := 0; i < len(opportunities); i++ {
|
||||
for j := i + 1; j < len(opportunities); j++ {
|
||||
iOpp := opportunities[i]
|
||||
jOpp := opportunities[j]
|
||||
|
||||
if jOpp.Urgency > iOpp.Urgency {
|
||||
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
|
||||
} else if jOpp.Urgency == iOpp.Urgency {
|
||||
if jOpp.EstimatedProfit.Cmp(iOpp.EstimatedProfit) > 0 {
|
||||
opportunities[i], opportunities[j] = opportunities[j], opportunities[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) calculateMinOutput(opportunity *ArbitrageOpportunity) *big.Int {
|
||||
expectedOutput := new(big.Int).Add(opportunity.RequiredAmount, opportunity.EstimatedProfit)
|
||||
|
||||
slippageTolerance := sas.config.SlippageTolerance
|
||||
slippageMultiplier := big.NewFloat(1.0 - slippageTolerance)
|
||||
|
||||
expectedFloat := new(big.Float).SetInt(expectedOutput)
|
||||
minOutputFloat := new(big.Float).Mul(expectedFloat, slippageMultiplier)
|
||||
|
||||
minOutput := new(big.Int)
|
||||
minOutputFloat.Int(minOutput)
|
||||
|
||||
return minOutput
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) processExecutionResult(result *ExecutionResult) {
|
||||
sas.statsMutex.Lock()
|
||||
if result.Success {
|
||||
sas.stats.TotalSuccessfulExecutions++
|
||||
sas.stats.TotalProfitRealized.Add(sas.stats.TotalProfitRealized, result.ProfitRealized)
|
||||
}
|
||||
|
||||
gasCost := new(big.Int).Mul(result.GasPrice, big.NewInt(int64(result.GasUsed)))
|
||||
sas.stats.TotalGasSpent.Add(sas.stats.TotalGasSpent, gasCost)
|
||||
sas.stats.LastExecutionTime = time.Now()
|
||||
sas.statsMutex.Unlock()
|
||||
|
||||
if err := sas.database.SaveExecution(sas.ctx, result); err != nil {
|
||||
sas.logger.Warn(fmt.Sprintf("Failed to save execution result to database: %v", err))
|
||||
}
|
||||
|
||||
if result.Success {
|
||||
sas.logger.Info(fmt.Sprintf("Arbitrage execution successful: TX %s, Profit: %s ETH, Gas: %d",
|
||||
result.TransactionHash.Hex(), formatEther(result.ProfitRealized), result.GasUsed))
|
||||
} else {
|
||||
sas.logger.Error(fmt.Sprintf("Arbitrage execution failed: TX %s, Error: %v",
|
||||
result.TransactionHash.Hex(), result.Error))
|
||||
}
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) statsUpdater() {
|
||||
defer sas.logger.Info("Stats updater stopped")
|
||||
|
||||
ticker := time.NewTicker(sas.config.StatsUpdateInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sas.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
sas.logStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) logStats() {
|
||||
sas.statsMutex.RLock()
|
||||
stats := *sas.stats
|
||||
sas.statsMutex.RUnlock()
|
||||
|
||||
successRate := 0.0
|
||||
if stats.TotalOpportunitiesExecuted > 0 {
|
||||
successRate = float64(stats.TotalSuccessfulExecutions) / float64(stats.TotalOpportunitiesExecuted) * 100
|
||||
}
|
||||
|
||||
sas.logger.Info(fmt.Sprintf("Arbitrage Service Stats - Detected: %d, Executed: %d, Success Rate: %.2f%%, "+
|
||||
"Total Profit: %s ETH, Total Gas: %s ETH",
|
||||
stats.TotalOpportunitiesDetected,
|
||||
stats.TotalOpportunitiesExecuted,
|
||||
successRate,
|
||||
formatEther(stats.TotalProfitRealized),
|
||||
formatEther(stats.TotalGasSpent)))
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) generateOpportunityID(path *ArbitragePath, event *SimpleSwapEvent) string {
|
||||
return fmt.Sprintf("%s_%s_%d", event.TxHash.Hex()[:10], path.Tokens[0].Hex()[:8], time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) GetStats() *ArbitrageStats {
|
||||
sas.statsMutex.RLock()
|
||||
defer sas.statsMutex.RUnlock()
|
||||
|
||||
statsCopy := *sas.stats
|
||||
return &statsCopy
|
||||
}
|
||||
|
||||
func (sas *SimpleArbitrageService) IsRunning() bool {
|
||||
sas.runMutex.RLock()
|
||||
defer sas.runMutex.RUnlock()
|
||||
return sas.isRunning
|
||||
}
|
||||
|
||||
// blockchainMonitor monitors the Arbitrum sequencer using the proper ArbitrumMonitor
|
||||
func (sas *SimpleArbitrageService) blockchainMonitor() {
|
||||
defer sas.logger.Info("Arbitrum sequencer monitor stopped")
|
||||
|
||||
sas.logger.Info("Starting Arbitrum sequencer monitor for MEV opportunities...")
|
||||
sas.logger.Info("Initializing Arbitrum L2 parser for transaction analysis...")
|
||||
|
||||
// Create the proper Arbitrum monitor with sequencer reader
|
||||
monitor, err := sas.createArbitrumMonitor()
|
||||
if err != nil {
|
||||
sas.logger.Error(fmt.Sprintf("Failed to create Arbitrum monitor: %v", err))
|
||||
// Fallback to basic block monitoring
|
||||
sas.fallbackBlockPolling()
|
||||
return
|
||||
}
|
||||
|
||||
sas.logger.Info("Arbitrum sequencer monitor created successfully")
|
||||
sas.logger.Info("Starting to monitor Arbitrum sequencer feed for transactions...")
|
||||
|
||||
// Start the monitor
|
||||
if err := monitor.Start(sas.ctx); err != nil {
|
||||
sas.logger.Error(fmt.Sprintf("Failed to start Arbitrum monitor: %v", err))
|
||||
sas.fallbackBlockPolling()
|
||||
return
|
||||
}
|
||||
|
||||
sas.logger.Info("Arbitrum sequencer monitoring started - processing live transactions")
|
||||
|
||||
// Keep the monitor running
|
||||
<-sas.ctx.Done()
|
||||
sas.logger.Info("Stopping Arbitrum sequencer monitor...")
|
||||
}
|
||||
|
||||
// fallbackBlockPolling provides fallback block monitoring through polling
|
||||
func (sas *SimpleArbitrageService) fallbackBlockPolling() {
|
||||
sas.logger.Info("Using fallback block polling...")
|
||||
|
||||
ticker := time.NewTicker(3 * time.Second) // Poll every 3 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
var lastBlock uint64
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sas.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
header, err := sas.client.HeaderByNumber(sas.ctx, nil)
|
||||
if err != nil {
|
||||
sas.logger.Debug(fmt.Sprintf("Failed to get latest block: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if header.Number.Uint64() > lastBlock {
|
||||
lastBlock = header.Number.Uint64()
|
||||
sas.processNewBlock(header)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processNewBlock processes a new block looking for swap events
|
||||
func (sas *SimpleArbitrageService) processNewBlock(header *types.Header) {
|
||||
blockNumber := header.Number.Uint64()
|
||||
|
||||
// Skip processing if block has no transactions
|
||||
if header.TxHash == (common.Hash{}) {
|
||||
return
|
||||
}
|
||||
|
||||
sas.logger.Info(fmt.Sprintf("Processing block %d for Uniswap V3 swap events", blockNumber))
|
||||
|
||||
// Instead of getting full block (which fails with unsupported tx types),
|
||||
// we'll scan the block's logs directly for Uniswap V3 Swap events
|
||||
swapEvents := sas.getSwapEventsFromBlock(blockNumber)
|
||||
|
||||
if len(swapEvents) > 0 {
|
||||
sas.logger.Info(fmt.Sprintf("Found %d swap events in block %d", len(swapEvents), blockNumber))
|
||||
|
||||
// Process each swap event
|
||||
for _, event := range swapEvents {
|
||||
go func(e *SimpleSwapEvent) {
|
||||
if err := sas.ProcessSwapEvent(e); err != nil {
|
||||
sas.logger.Debug(fmt.Sprintf("Failed to process swap event: %v", err))
|
||||
}
|
||||
}(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processTransaction analyzes a transaction for swap events
|
||||
func (sas *SimpleArbitrageService) processTransaction(tx *types.Transaction, blockNumber uint64) bool {
|
||||
// Get transaction receipt to access logs
|
||||
receipt, err := sas.client.TransactionReceipt(sas.ctx, tx.Hash())
|
||||
if err != nil {
|
||||
return false // Skip if we can't get receipt
|
||||
}
|
||||
|
||||
swapFound := false
|
||||
// Look for Uniswap V3 Swap events
|
||||
for _, log := range receipt.Logs {
|
||||
event := sas.parseSwapLog(log, tx, blockNumber)
|
||||
if event != nil {
|
||||
swapFound = true
|
||||
sas.logger.Info(fmt.Sprintf("Found swap event: %s/%s, amounts: %s/%s",
|
||||
event.Token0.Hex()[:10], event.Token1.Hex()[:10],
|
||||
event.Amount0.String(), event.Amount1.String()))
|
||||
|
||||
// Process the swap event asynchronously to avoid blocking
|
||||
go func(e *SimpleSwapEvent) {
|
||||
if err := sas.ProcessSwapEvent(e); err != nil {
|
||||
sas.logger.Debug(fmt.Sprintf("Failed to process swap event: %v", err))
|
||||
}
|
||||
}(event)
|
||||
}
|
||||
}
|
||||
return swapFound
|
||||
}
|
||||
|
||||
// parseSwapLog attempts to parse a log as a Uniswap V3 Swap event
|
||||
func (sas *SimpleArbitrageService) parseSwapLog(log *types.Log, tx *types.Transaction, blockNumber uint64) *SimpleSwapEvent {
|
||||
// Uniswap V3 Pool Swap event signature
|
||||
// Swap(indexed address sender, indexed address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)
|
||||
swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")
|
||||
|
||||
if len(log.Topics) == 0 || log.Topics[0] != swapEventSig {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the event data
|
||||
if len(log.Topics) < 3 || len(log.Data) < 192 { // 6 * 32 bytes
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract indexed parameters (sender, recipient)
|
||||
// sender := common.BytesToAddress(log.Topics[1].Bytes())
|
||||
// recipient := common.BytesToAddress(log.Topics[2].Bytes())
|
||||
|
||||
// Extract non-indexed parameters from data
|
||||
amount0 := new(big.Int).SetBytes(log.Data[0:32])
|
||||
amount1 := new(big.Int).SetBytes(log.Data[32:64])
|
||||
sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96])
|
||||
liquidity := new(big.Int).SetBytes(log.Data[96:128])
|
||||
|
||||
// Extract tick (int24, but stored as int256)
|
||||
tickBytes := log.Data[128:160]
|
||||
tick := new(big.Int).SetBytes(tickBytes)
|
||||
if tick.Bit(255) == 1 { // Check if negative (two's complement)
|
||||
tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256))
|
||||
}
|
||||
|
||||
// Get pool tokens by querying the actual pool contract
|
||||
token0, token1, err := sas.getPoolTokens(log.Address)
|
||||
if err != nil {
|
||||
return nil // Skip if we can't get pool tokens
|
||||
}
|
||||
|
||||
return &SimpleSwapEvent{
|
||||
TxHash: tx.Hash(),
|
||||
PoolAddress: log.Address,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Amount0: amount0,
|
||||
Amount1: amount1,
|
||||
SqrtPriceX96: sqrtPriceX96,
|
||||
Liquidity: liquidity,
|
||||
Tick: int32(tick.Int64()),
|
||||
BlockNumber: blockNumber,
|
||||
LogIndex: log.Index,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// getPoolTokens retrieves token addresses for a Uniswap V3 pool with caching
|
||||
func (sas *SimpleArbitrageService) getPoolTokens(poolAddress common.Address) (token0, token1 common.Address, err error) {
|
||||
// Check cache first
|
||||
sas.tokenCacheMutex.RLock()
|
||||
if cached, exists := sas.tokenCache[poolAddress]; exists {
|
||||
sas.tokenCacheMutex.RUnlock()
|
||||
return cached.Token0, cached.Token1, nil
|
||||
}
|
||||
sas.tokenCacheMutex.RUnlock()
|
||||
|
||||
// Create timeout context for contract calls
|
||||
ctx, cancel := context.WithTimeout(sas.ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Pre-computed function selectors for token0() and token1()
|
||||
token0Selector := []byte{0x0d, 0xfe, 0x16, 0x81} // token0()
|
||||
token1Selector := []byte{0xd2, 0x1c, 0xec, 0xd4} // token1()
|
||||
|
||||
// Call token0() function
|
||||
token0Data, err := sas.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddress,
|
||||
Data: token0Selector,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, common.Address{}, fmt.Errorf("failed to call token0(): %w", err)
|
||||
}
|
||||
|
||||
// Call token1() function
|
||||
token1Data, err := sas.client.CallContract(ctx, ethereum.CallMsg{
|
||||
To: &poolAddress,
|
||||
Data: token1Selector,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return common.Address{}, common.Address{}, fmt.Errorf("failed to call token1(): %w", err)
|
||||
}
|
||||
|
||||
// Parse the results
|
||||
if len(token0Data) < 32 || len(token1Data) < 32 {
|
||||
return common.Address{}, common.Address{}, fmt.Errorf("invalid token data length")
|
||||
}
|
||||
|
||||
token0 = common.BytesToAddress(token0Data[12:32])
|
||||
token1 = common.BytesToAddress(token1Data[12:32])
|
||||
|
||||
// Cache the result
|
||||
sas.tokenCacheMutex.Lock()
|
||||
sas.tokenCache[poolAddress] = TokenPair{Token0: token0, Token1: token1}
|
||||
sas.tokenCacheMutex.Unlock()
|
||||
|
||||
return token0, token1, nil
|
||||
}
|
||||
|
||||
// getSwapEventsFromBlock retrieves Uniswap V3 swap events from a specific block using log filtering
|
||||
func (sas *SimpleArbitrageService) getSwapEventsFromBlock(blockNumber uint64) []*SimpleSwapEvent {
|
||||
// Uniswap V3 Pool Swap event signature
|
||||
swapEventSig := common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")
|
||||
|
||||
// Create filter query for this specific block
|
||||
query := ethereum.FilterQuery{
|
||||
FromBlock: big.NewInt(int64(blockNumber)),
|
||||
ToBlock: big.NewInt(int64(blockNumber)),
|
||||
Topics: [][]common.Hash{{swapEventSig}},
|
||||
}
|
||||
|
||||
// Get logs for this block
|
||||
logs, err := sas.client.FilterLogs(sas.ctx, query)
|
||||
if err != nil {
|
||||
sas.logger.Debug(fmt.Sprintf("Failed to get logs for block %d: %v", blockNumber, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug: Log how many logs we found for this block
|
||||
if len(logs) > 0 {
|
||||
sas.logger.Info(fmt.Sprintf("Found %d potential swap logs in block %d", len(logs), blockNumber))
|
||||
}
|
||||
|
||||
var swapEvents []*SimpleSwapEvent
|
||||
|
||||
// Parse each log into a swap event
|
||||
for _, log := range logs {
|
||||
event := sas.parseSwapEvent(log, blockNumber)
|
||||
if event != nil {
|
||||
swapEvents = append(swapEvents, event)
|
||||
sas.logger.Info(fmt.Sprintf("Successfully parsed swap event: pool=%s, amount0=%s, amount1=%s",
|
||||
event.PoolAddress.Hex(), event.Amount0.String(), event.Amount1.String()))
|
||||
} else {
|
||||
sas.logger.Debug(fmt.Sprintf("Failed to parse swap log from pool %s", log.Address.Hex()))
|
||||
}
|
||||
}
|
||||
|
||||
return swapEvents
|
||||
}
|
||||
|
||||
// parseSwapEvent parses a log entry into a SimpleSwapEvent
|
||||
func (sas *SimpleArbitrageService) parseSwapEvent(log types.Log, blockNumber uint64) *SimpleSwapEvent {
|
||||
// Validate log structure
|
||||
if len(log.Topics) < 3 || len(log.Data) < 192 { // 6 * 32 bytes
|
||||
sas.logger.Debug(fmt.Sprintf("Invalid log structure: topics=%d, data_len=%d", len(log.Topics), len(log.Data)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract non-indexed parameters from data
|
||||
amount0 := new(big.Int).SetBytes(log.Data[0:32])
|
||||
amount1 := new(big.Int).SetBytes(log.Data[32:64])
|
||||
sqrtPriceX96 := new(big.Int).SetBytes(log.Data[64:96])
|
||||
liquidity := new(big.Int).SetBytes(log.Data[96:128])
|
||||
|
||||
// Extract tick (int24, but stored as int256)
|
||||
tickBytes := log.Data[128:160]
|
||||
tick := new(big.Int).SetBytes(tickBytes)
|
||||
if tick.Bit(255) == 1 { // Check if negative (two's complement)
|
||||
tick.Sub(tick, new(big.Int).Lsh(big.NewInt(1), 256))
|
||||
}
|
||||
|
||||
// Get pool tokens by querying the actual pool contract
|
||||
token0, token1, err := sas.getPoolTokens(log.Address)
|
||||
if err != nil {
|
||||
sas.logger.Error(fmt.Sprintf("Failed to get tokens for pool %s: %v", log.Address.Hex(), err))
|
||||
return nil // Skip if we can't get pool tokens
|
||||
}
|
||||
|
||||
sas.logger.Debug(fmt.Sprintf("Successfully got pool tokens: %s/%s for pool %s",
|
||||
token0.Hex(), token1.Hex(), log.Address.Hex()))
|
||||
|
||||
return &SimpleSwapEvent{
|
||||
TxHash: log.TxHash,
|
||||
PoolAddress: log.Address,
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Amount0: amount0,
|
||||
Amount1: amount1,
|
||||
SqrtPriceX96: sqrtPriceX96,
|
||||
Liquidity: liquidity,
|
||||
Tick: int32(tick.Int64()),
|
||||
BlockNumber: blockNumber,
|
||||
LogIndex: log.Index,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user