feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
423
orig/pkg/arbitrage/database.go
Normal file
423
orig/pkg/arbitrage/database.go
Normal file
@@ -0,0 +1,423 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
// 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 *pkgtypes.ArbitrageOpportunity) error {
|
||||
pathJSON, err := json.Marshal(opportunity.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal path: %w", err)
|
||||
}
|
||||
|
||||
// Create empty trigger event for compatibility
|
||||
triggerEvent := map[string]interface{}{
|
||||
"protocol": opportunity.Protocol,
|
||||
"tokenIn": opportunity.TokenIn.Hex(),
|
||||
"tokenOut": opportunity.TokenOut.Hex(),
|
||||
}
|
||||
eventJSON, err := json.Marshal(triggerEvent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal trigger event: %w", err)
|
||||
}
|
||||
|
||||
// Generate a simple ID from timestamp and token addresses
|
||||
opportunityID := fmt.Sprintf("%s_%s_%d",
|
||||
opportunity.TokenIn.Hex()[:8],
|
||||
opportunity.TokenOut.Hex()[:8],
|
||||
opportunity.Timestamp)
|
||||
|
||||
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,
|
||||
opportunityID,
|
||||
string(pathJSON),
|
||||
string(eventJSON),
|
||||
opportunity.Timestamp,
|
||||
opportunity.Profit.String(),
|
||||
opportunity.AmountIn.String(),
|
||||
1, // Default urgency
|
||||
opportunity.Timestamp+3600, // Expires in 1 hour
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save opportunity: %w", err)
|
||||
}
|
||||
|
||||
db.logger.Debug(fmt.Sprintf("Saved arbitrage opportunity %s to database", opportunityID))
|
||||
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("execution error: %s", *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())
|
||||
}
|
||||
90
orig/pkg/arbitrage/decimal_helpers.go
Normal file
90
orig/pkg/arbitrage/decimal_helpers.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
)
|
||||
|
||||
var sharedDecimalConverter = math.NewDecimalConverter()
|
||||
|
||||
// universalFromWei converts a wei-denominated big.Int into a UniversalDecimal with 18 decimals.
|
||||
// It gracefully handles nil values by returning a zero amount.
|
||||
func universalFromWei(dec *math.DecimalConverter, value *big.Int, symbol string) *math.UniversalDecimal {
|
||||
if dec == nil {
|
||||
dec = sharedDecimalConverter
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
zero, _ := math.NewUniversalDecimal(big.NewInt(0), 18, symbol)
|
||||
return zero
|
||||
}
|
||||
|
||||
return dec.FromWei(value, 18, symbol)
|
||||
}
|
||||
|
||||
func universalOrFromWei(dec *math.DecimalConverter, decimal *math.UniversalDecimal, fallback *big.Int, decimals uint8, symbol string) *math.UniversalDecimal {
|
||||
if decimal != nil {
|
||||
return decimal
|
||||
}
|
||||
|
||||
if fallback == nil {
|
||||
zero, _ := math.NewUniversalDecimal(big.NewInt(0), decimals, symbol)
|
||||
return zero
|
||||
}
|
||||
|
||||
if dec == nil {
|
||||
dc := sharedDecimalConverter
|
||||
return dc.FromWei(fallback, decimals, symbol)
|
||||
}
|
||||
|
||||
return dec.FromWei(fallback, decimals, symbol)
|
||||
}
|
||||
|
||||
func floatStringFromDecimal(ud *math.UniversalDecimal, precision int) string {
|
||||
if precision < 0 {
|
||||
precision = int(ud.Decimals)
|
||||
}
|
||||
|
||||
if ud == nil {
|
||||
if precision <= 0 {
|
||||
return "0"
|
||||
}
|
||||
return fmt.Sprintf("0.%0*d", precision, 0)
|
||||
}
|
||||
|
||||
if ud.Decimals == 0 {
|
||||
return ud.Value.String()
|
||||
}
|
||||
|
||||
numerator := new(big.Int).Set(ud.Value)
|
||||
denominator := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(ud.Decimals)), nil)
|
||||
rat := new(big.Rat).SetFrac(numerator, denominator)
|
||||
return rat.FloatString(precision)
|
||||
}
|
||||
|
||||
func ethAmountString(dec *math.DecimalConverter, decimal *math.UniversalDecimal, wei *big.Int) string {
|
||||
if dec == nil {
|
||||
dec = sharedDecimalConverter
|
||||
}
|
||||
ud := universalOrFromWei(dec, decimal, wei, 18, "ETH")
|
||||
return floatStringFromDecimal(ud, 6)
|
||||
}
|
||||
|
||||
func gweiAmountString(dec *math.DecimalConverter, decimal *math.UniversalDecimal, wei *big.Int) string {
|
||||
if dec == nil {
|
||||
dec = sharedDecimalConverter
|
||||
}
|
||||
if decimal != nil {
|
||||
return floatStringFromDecimal(decimal, 2)
|
||||
}
|
||||
if wei == nil {
|
||||
return "0.00"
|
||||
}
|
||||
numerator := new(big.Rat).SetInt(wei)
|
||||
denominator := new(big.Rat).SetInt(big.NewInt(1_000_000_000))
|
||||
return new(big.Rat).Quo(numerator, denominator).FloatString(2)
|
||||
}
|
||||
|
||||
// Removed unused format functions - moved to examples/profitability_demo.go if needed
|
||||
38
orig/pkg/arbitrage/decimal_helpers_test.go
Normal file
38
orig/pkg/arbitrage/decimal_helpers_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
)
|
||||
|
||||
func TestEthAmountStringPrefersDecimalSnapshot(t *testing.T) {
|
||||
ud, err := math.NewUniversalDecimal(big.NewInt(1500000000000000000), 18, "ETH")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating decimal: %v", err)
|
||||
}
|
||||
|
||||
got := ethAmountString(nil, ud, nil)
|
||||
if got != "1.500000" {
|
||||
t.Fatalf("expected 1.500000, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEthAmountStringFallsBackToWei(t *testing.T) {
|
||||
wei := big.NewInt(123450000000000000)
|
||||
|
||||
got := ethAmountString(nil, nil, wei)
|
||||
if got != "0.123450" {
|
||||
t.Fatalf("expected 0.123450, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGweiAmountStringFormatsTwoDecimals(t *testing.T) {
|
||||
wei := big.NewInt(987654321)
|
||||
|
||||
got := gweiAmountString(nil, nil, wei)
|
||||
if got != "0.99" {
|
||||
t.Fatalf("expected 0.99, got %s", got)
|
||||
}
|
||||
}
|
||||
975
orig/pkg/arbitrage/detection_engine.go
Normal file
975
orig/pkg/arbitrage/detection_engine.go
Normal file
@@ -0,0 +1,975 @@
|
||||
// Package arbitrage provides the core arbitrage detection and analysis engine
|
||||
// for the MEV bot. This package is responsible for identifying profitable
|
||||
// arbitrage opportunities across multiple DEX protocols on Arbitrum.
|
||||
//
|
||||
// The detection engine continuously scans for price discrepancies between
|
||||
// exchanges and calculates potential profits after accounting for gas costs,
|
||||
// slippage, and other factors. It uses advanced mathematical models to
|
||||
// optimize trade sizing and minimize risks.
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/exchanges"
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
// ArbitrageDetectionEngine is the core component responsible for discovering
|
||||
// profitable arbitrage opportunities in real-time across multiple DEX protocols.
|
||||
// The engine uses sophisticated algorithms to:
|
||||
//
|
||||
// 1. Continuously scan token pairs across supported exchanges
|
||||
// 2. Identify price discrepancies that exceed minimum profit thresholds
|
||||
// 3. Calculate optimal trade sizes considering slippage and gas costs
|
||||
// 4. Filter opportunities based on risk and confidence metrics
|
||||
// 5. Provide real-time opportunity feeds to execution systems
|
||||
//
|
||||
// The engine operates with configurable parameters for different trading strategies
|
||||
// and risk profiles, making it suitable for both conservative and aggressive MEV extraction.
|
||||
type ArbitrageDetectionEngine struct {
|
||||
// Core dependencies for arbitrage detection
|
||||
registry *exchanges.ExchangeRegistry // Registry of supported exchanges and their configurations
|
||||
calculator *math.ArbitrageCalculator // Mathematical engine for profit calculations
|
||||
gasEstimator math.GasEstimator // Gas cost estimation for transaction profitability
|
||||
logger *logger.Logger // Structured logging for monitoring and debugging
|
||||
decimalConverter *math.DecimalConverter // Handles precision math for token amounts
|
||||
|
||||
// Callback function for handling discovered opportunities
|
||||
// This is typically connected to an execution engine
|
||||
opportunityHandler func(*types.ArbitrageOpportunity)
|
||||
|
||||
// Configuration parameters that control detection behavior
|
||||
config DetectionConfig
|
||||
|
||||
// State management for concurrent operation
|
||||
runningMutex sync.RWMutex // Protects running state from race conditions
|
||||
isRunning bool // Indicates if the engine is currently active
|
||||
stopChan chan struct{} // Channel for graceful shutdown signaling
|
||||
opportunityChan chan *types.ArbitrageOpportunity // Buffered channel for opportunity distribution
|
||||
|
||||
// Performance tracking and metrics
|
||||
scanCount uint64 // Total number of scans performed since startup
|
||||
opportunityCount uint64 // Total number of opportunities discovered
|
||||
lastScanTime time.Time // Timestamp of the most recent scan completion
|
||||
|
||||
// Concurrent processing infrastructure
|
||||
scanWorkers *WorkerPool // Pool of workers for parallel opportunity scanning
|
||||
pathWorkers *WorkerPool // Pool of workers for complex path analysis
|
||||
|
||||
// CRITICAL FIX: Backpressure for opportunity handlers
|
||||
// Prevents unbounded goroutine creation under high opportunity rate
|
||||
handlerSemaphore chan struct{} // Limits concurrent handler executions
|
||||
maxHandlers int // Maximum concurrent handler goroutines
|
||||
}
|
||||
|
||||
// DetectionConfig contains all configuration parameters for the arbitrage detection engine.
|
||||
// These parameters control scanning behavior, opportunity filtering, and performance characteristics.
|
||||
// The configuration allows fine-tuning for different trading strategies and risk profiles.
|
||||
type DetectionConfig struct {
|
||||
// Scanning timing and concurrency parameters
|
||||
ScanInterval time.Duration // How frequently to scan for new opportunities
|
||||
MaxConcurrentScans int // Maximum number of simultaneous scanning operations
|
||||
MaxConcurrentPaths int // Maximum number of paths to analyze in parallel
|
||||
|
||||
// Opportunity filtering criteria - these determine what qualifies as actionable
|
||||
MinProfitThreshold *math.UniversalDecimal // Minimum profit required to consider an opportunity
|
||||
MaxPriceImpact *math.UniversalDecimal // Maximum acceptable price impact (slippage)
|
||||
MaxHops int // Maximum number of hops in a trading path
|
||||
|
||||
// Token filtering and prioritization
|
||||
HighPriorityTokens []common.Address // Tokens to scan more frequently (e.g., WETH, USDC)
|
||||
TokenWhitelist []common.Address // Only scan these tokens if specified
|
||||
TokenBlacklist []common.Address // Never scan these tokens (e.g., known scam tokens)
|
||||
|
||||
// Exchange selection and weighting
|
||||
EnabledExchanges []math.ExchangeType // Which exchanges to include in scanning
|
||||
ExchangeWeights map[math.ExchangeType]float64 // Relative weights for exchange prioritization
|
||||
|
||||
// Performance optimization settings
|
||||
CachePoolData bool // Whether to cache pool data between scans
|
||||
CacheTTL time.Duration // How long to keep cached data valid
|
||||
BatchSize int // Number of opportunities to process in each batch
|
||||
|
||||
// Risk management constraints
|
||||
MaxPositionSize *math.UniversalDecimal // Maximum size for any single arbitrage
|
||||
RequiredConfidence float64 // Minimum confidence score (0.0-1.0) for execution
|
||||
}
|
||||
|
||||
// WorkerPool manages concurrent workers for scanning
|
||||
type WorkerPool struct {
|
||||
workers int
|
||||
taskChan chan ScanTask
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// ScanTask represents a scanning task
|
||||
type ScanTask struct {
|
||||
TokenPair exchanges.TokenPair
|
||||
Exchanges []*exchanges.ExchangeConfig
|
||||
InputAmount *math.UniversalDecimal
|
||||
ResultChan chan ScanResult
|
||||
}
|
||||
|
||||
// ScanResult contains the result of a scanning task
|
||||
type ScanResult struct {
|
||||
Opportunity *types.ArbitrageOpportunity
|
||||
Error error
|
||||
ScanTime time.Duration
|
||||
}
|
||||
|
||||
// NewArbitrageDetectionEngine creates and initializes a new arbitrage detection engine
|
||||
// with the specified dependencies and configuration. The engine is created in a stopped
|
||||
// state and must be started explicitly using the Start() method.
|
||||
//
|
||||
// Parameters:
|
||||
// - registry: Exchange registry containing supported DEX configurations
|
||||
// - gasEstimator: Component for estimating transaction gas costs
|
||||
// - logger: Structured logger for monitoring and debugging
|
||||
// - config: Configuration parameters for detection behavior
|
||||
//
|
||||
// Returns:
|
||||
// - *ArbitrageDetectionEngine: Configured but not yet started detection engine
|
||||
func NewArbitrageDetectionEngine(
|
||||
registry *exchanges.ExchangeRegistry,
|
||||
gasEstimator math.GasEstimator,
|
||||
logger *logger.Logger,
|
||||
config DetectionConfig,
|
||||
) *ArbitrageDetectionEngine {
|
||||
|
||||
calculator := math.NewArbitrageCalculator(gasEstimator)
|
||||
|
||||
engine := &ArbitrageDetectionEngine{
|
||||
registry: registry,
|
||||
calculator: calculator,
|
||||
gasEstimator: gasEstimator,
|
||||
logger: logger,
|
||||
decimalConverter: math.NewDecimalConverter(),
|
||||
config: config,
|
||||
isRunning: false,
|
||||
stopChan: make(chan struct{}),
|
||||
opportunityChan: make(chan *types.ArbitrageOpportunity, 1000), // Buffered channel
|
||||
maxHandlers: 10, // CRITICAL FIX: Limit to 10 concurrent handlers
|
||||
handlerSemaphore: make(chan struct{}, 10), // CRITICAL FIX: Backpressure semaphore
|
||||
}
|
||||
|
||||
// Set default configuration if not provided
|
||||
engine.setDefaultConfig()
|
||||
|
||||
return engine
|
||||
}
|
||||
|
||||
// setDefaultConfig sets default configuration values
|
||||
func (engine *ArbitrageDetectionEngine) setDefaultConfig() {
|
||||
if engine.config.ScanInterval == 0 {
|
||||
engine.config.ScanInterval = 1 * time.Second
|
||||
}
|
||||
|
||||
if engine.config.MaxConcurrentScans == 0 {
|
||||
engine.config.MaxConcurrentScans = 10
|
||||
}
|
||||
|
||||
if engine.config.MaxConcurrentPaths == 0 {
|
||||
engine.config.MaxConcurrentPaths = 50
|
||||
}
|
||||
|
||||
if engine.config.MinProfitThreshold == nil {
|
||||
// CRITICAL FIX #1: Reduce minimum profit to 0.00005 ETH (realistic threshold)
|
||||
// Arbitrum has low gas costs: ~100k-200k gas @ 0.1-0.2 gwei = ~0.0001-0.0002 ETH
|
||||
// 0.00005 ETH provides ~2-3x gas cost safety margin (optimal for profitability)
|
||||
// Previous 0.001 ETH threshold killed 95% of viable opportunities
|
||||
engine.config.MinProfitThreshold, _ = engine.decimalConverter.FromString("0.00005", 18, "ETH")
|
||||
}
|
||||
|
||||
if engine.config.MaxPriceImpact == nil {
|
||||
engine.config.MaxPriceImpact, _ = engine.decimalConverter.FromString("2", 4, "PERCENT")
|
||||
}
|
||||
|
||||
if engine.config.MaxHops == 0 {
|
||||
engine.config.MaxHops = 3
|
||||
}
|
||||
|
||||
if engine.config.CacheTTL == 0 {
|
||||
engine.config.CacheTTL = 30 * time.Second
|
||||
}
|
||||
|
||||
if engine.config.BatchSize == 0 {
|
||||
engine.config.BatchSize = 20
|
||||
}
|
||||
|
||||
if engine.config.RequiredConfidence == 0 {
|
||||
engine.config.RequiredConfidence = 0.7
|
||||
}
|
||||
|
||||
if len(engine.config.EnabledExchanges) == 0 {
|
||||
// Enable all exchanges by default
|
||||
for _, exchangeConfig := range engine.registry.GetAllExchanges() {
|
||||
engine.config.EnabledExchanges = append(engine.config.EnabledExchanges, exchangeConfig.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the arbitrage detection process and initializes all background workers.
|
||||
// This method starts the main detection loop, worker pools, and opportunity processing.
|
||||
// The engine will continue running until Stop() is called or the context is cancelled.
|
||||
//
|
||||
// The startup process includes:
|
||||
// 1. Validating that the engine is not already running
|
||||
// 2. Initializing worker pools for concurrent processing
|
||||
// 3. Starting the main detection loop
|
||||
// 4. Starting the opportunity processing pipeline
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for controlling the engine lifecycle
|
||||
//
|
||||
// Returns:
|
||||
// - error: Any startup errors
|
||||
func (engine *ArbitrageDetectionEngine) Start(ctx context.Context) error {
|
||||
// Ensure thread-safe state management during startup
|
||||
engine.runningMutex.Lock()
|
||||
defer engine.runningMutex.Unlock()
|
||||
|
||||
if engine.isRunning {
|
||||
return fmt.Errorf("detection engine is already running")
|
||||
}
|
||||
|
||||
engine.logger.Info("Starting arbitrage detection engine...")
|
||||
engine.logger.Info(fmt.Sprintf("Configuration - Scan Interval: %v, Max Concurrent Scans: %d, Min Profit: %s ETH",
|
||||
engine.config.ScanInterval,
|
||||
engine.config.MaxConcurrentScans,
|
||||
engine.decimalConverter.ToHumanReadable(engine.config.MinProfitThreshold)))
|
||||
|
||||
// Initialize worker pools
|
||||
if err := engine.initializeWorkerPools(ctx); err != nil {
|
||||
return fmt.Errorf("failed to initialize worker pools: %w", err)
|
||||
}
|
||||
|
||||
engine.isRunning = true
|
||||
|
||||
// Start main detection loop
|
||||
go engine.detectionLoop(ctx)
|
||||
|
||||
// Start opportunity processing
|
||||
go engine.opportunityProcessor(ctx)
|
||||
|
||||
engine.logger.Info("Arbitrage detection engine started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop halts the arbitrage detection process
|
||||
func (engine *ArbitrageDetectionEngine) Stop() error {
|
||||
engine.runningMutex.Lock()
|
||||
defer engine.runningMutex.Unlock()
|
||||
|
||||
if !engine.isRunning {
|
||||
return fmt.Errorf("detection engine is not running")
|
||||
}
|
||||
|
||||
engine.logger.Info("Stopping arbitrage detection engine...")
|
||||
|
||||
// Signal stop
|
||||
close(engine.stopChan)
|
||||
|
||||
// Stop worker pools
|
||||
if engine.scanWorkers != nil {
|
||||
engine.scanWorkers.Stop()
|
||||
}
|
||||
if engine.pathWorkers != nil {
|
||||
engine.pathWorkers.Stop()
|
||||
}
|
||||
|
||||
engine.isRunning = false
|
||||
|
||||
engine.logger.Info(fmt.Sprintf("Detection engine stopped. Total scans: %d, Opportunities found: %d",
|
||||
engine.scanCount, engine.opportunityCount))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializeWorkerPools sets up worker pools for concurrent processing
|
||||
func (engine *ArbitrageDetectionEngine) initializeWorkerPools(ctx context.Context) error {
|
||||
// Initialize scan worker pool
|
||||
engine.scanWorkers = NewWorkerPool(engine.config.MaxConcurrentScans, ctx)
|
||||
engine.scanWorkers.Start(engine.processScanTask)
|
||||
|
||||
// Initialize path worker pool
|
||||
engine.pathWorkers = NewWorkerPool(engine.config.MaxConcurrentPaths, ctx)
|
||||
engine.pathWorkers.Start(engine.processPathTask)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectionLoop runs the main detection logic
|
||||
func (engine *ArbitrageDetectionEngine) detectionLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(engine.config.ScanInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
engine.logger.Info("Detection loop stopped due to context cancellation")
|
||||
return
|
||||
case <-engine.stopChan:
|
||||
engine.logger.Info("Detection loop stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
engine.performScan(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performScan executes a complete arbitrage scanning cycle across all configured
|
||||
// token pairs and exchanges. This is the core scanning logic that runs periodically
|
||||
// to discover new arbitrage opportunities.
|
||||
//
|
||||
// The scanning process includes:
|
||||
// 1. Identifying token pairs to analyze
|
||||
// 2. Determining input amounts to test
|
||||
// 3. Creating scanning tasks for worker pools
|
||||
// 4. Processing results and filtering opportunities
|
||||
//
|
||||
// Each scan is tracked for performance monitoring and optimization.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: Context for cancellation and timeout control
|
||||
func (engine *ArbitrageDetectionEngine) performScan(ctx context.Context) {
|
||||
// Track scan performance for monitoring
|
||||
scanStart := time.Now()
|
||||
engine.scanCount++ // Increment total scan counter
|
||||
|
||||
engine.logger.Debug(fmt.Sprintf("Starting arbitrage scan #%d", engine.scanCount))
|
||||
|
||||
// Get token pairs to scan
|
||||
tokenPairs := engine.getTokenPairsToScan()
|
||||
|
||||
// Get input amounts to test
|
||||
inputAmounts := engine.getInputAmountsToTest()
|
||||
|
||||
// Create scan tasks
|
||||
scanTasks := make([]ScanTask, 0)
|
||||
|
||||
for _, pair := range tokenPairs {
|
||||
// Get exchanges that support this pair
|
||||
supportingExchanges := engine.registry.GetExchangesForPair(
|
||||
common.HexToAddress(pair.Token0.Address),
|
||||
common.HexToAddress(pair.Token1.Address),
|
||||
)
|
||||
|
||||
// Filter enabled exchanges
|
||||
enabledExchanges := engine.filterEnabledExchanges(supportingExchanges)
|
||||
|
||||
if len(enabledExchanges) < 2 {
|
||||
continue // Need at least 2 exchanges for arbitrage
|
||||
}
|
||||
|
||||
for _, inputAmount := range inputAmounts {
|
||||
task := ScanTask{
|
||||
TokenPair: pair,
|
||||
Exchanges: enabledExchanges,
|
||||
InputAmount: inputAmount,
|
||||
ResultChan: make(chan ScanResult, 1),
|
||||
}
|
||||
scanTasks = append(scanTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
engine.logger.Debug(fmt.Sprintf("Created %d scan tasks for %d token pairs", len(scanTasks), len(tokenPairs)))
|
||||
|
||||
// Process scan tasks in batches
|
||||
engine.processScanTasksBatch(ctx, scanTasks)
|
||||
|
||||
scanDuration := time.Since(scanStart)
|
||||
engine.lastScanTime = time.Now()
|
||||
|
||||
engine.logger.Debug(fmt.Sprintf("Completed arbitrage scan #%d in %v", engine.scanCount, scanDuration))
|
||||
}
|
||||
|
||||
// getTokenPairsToScan returns token pairs to scan for arbitrage
|
||||
func (engine *ArbitrageDetectionEngine) getTokenPairsToScan() []exchanges.TokenPair {
|
||||
// Get high priority tokens first
|
||||
highPriorityTokens := engine.registry.GetHighPriorityTokens(10)
|
||||
|
||||
// Create pairs from high priority tokens
|
||||
pairs := make([]exchanges.TokenPair, 0)
|
||||
|
||||
for i, token0 := range highPriorityTokens {
|
||||
for j, token1 := range highPriorityTokens {
|
||||
if i >= j {
|
||||
continue // Avoid duplicates and self-pairs
|
||||
}
|
||||
|
||||
// Check if pair is supported
|
||||
if engine.registry.IsPairSupported(
|
||||
common.HexToAddress(token0.Address),
|
||||
common.HexToAddress(token1.Address),
|
||||
) {
|
||||
pairs = append(pairs, exchanges.TokenPair{
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
// getInputAmountsToTest returns different input amounts to test for arbitrage
|
||||
func (engine *ArbitrageDetectionEngine) getInputAmountsToTest() []*math.UniversalDecimal {
|
||||
amounts := make([]*math.UniversalDecimal, 0)
|
||||
|
||||
// Test different input amounts to find optimal arbitrage size
|
||||
testAmounts := []string{"0.1", "0.5", "1", "2", "5", "10"}
|
||||
|
||||
for _, amountStr := range testAmounts {
|
||||
if amount, err := engine.decimalConverter.FromString(amountStr, 18, "ETH"); err == nil {
|
||||
amounts = append(amounts, amount)
|
||||
}
|
||||
}
|
||||
|
||||
return amounts
|
||||
}
|
||||
|
||||
// filterEnabledExchanges filters exchanges based on configuration
|
||||
func (engine *ArbitrageDetectionEngine) filterEnabledExchanges(exchangeConfigs []*exchanges.ExchangeConfig) []*exchanges.ExchangeConfig {
|
||||
enabled := make([]*exchanges.ExchangeConfig, 0)
|
||||
|
||||
enabledMap := make(map[math.ExchangeType]bool)
|
||||
for _, exchangeType := range engine.config.EnabledExchanges {
|
||||
enabledMap[exchangeType] = true
|
||||
}
|
||||
|
||||
for _, exchange := range exchangeConfigs {
|
||||
if enabledMap[exchange.Type] {
|
||||
enabled = append(enabled, exchange)
|
||||
}
|
||||
}
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
// processScanTasksBatch processes scan tasks in batches for efficiency
|
||||
func (engine *ArbitrageDetectionEngine) processScanTasksBatch(ctx context.Context, tasks []ScanTask) {
|
||||
batchSize := engine.config.BatchSize
|
||||
|
||||
for i := 0; i < len(tasks); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(tasks) {
|
||||
end = len(tasks)
|
||||
}
|
||||
|
||||
batch := tasks[i:end]
|
||||
engine.processScanBatch(ctx, batch)
|
||||
|
||||
// Small delay between batches to avoid overwhelming the system
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processScanBatch processes a batch of scan tasks concurrently
|
||||
func (engine *ArbitrageDetectionEngine) processScanBatch(ctx context.Context, batch []ScanTask) {
|
||||
resultChans := make([]chan ScanResult, len(batch))
|
||||
|
||||
// Submit tasks to worker pool
|
||||
for i, task := range batch {
|
||||
resultChans[i] = task.ResultChan
|
||||
|
||||
select {
|
||||
case engine.scanWorkers.taskChan <- task:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Collect results
|
||||
for _, resultChan := range resultChans {
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.Error != nil {
|
||||
engine.logger.Debug(fmt.Sprintf("Scan task error: %v", result.Error))
|
||||
continue
|
||||
}
|
||||
|
||||
if result.Opportunity != nil && engine.calculator.IsOpportunityProfitable(result.Opportunity) {
|
||||
engine.opportunityCount++
|
||||
|
||||
// Send opportunity to processing channel
|
||||
select {
|
||||
case engine.opportunityChan <- result.Opportunity:
|
||||
profitDisplay := ethAmountString(engine.decimalConverter, nil, result.Opportunity.NetProfit)
|
||||
engine.logger.Info(fmt.Sprintf("🎯 Found profitable arbitrage: %s ETH profit, %0.1f%% confidence",
|
||||
profitDisplay,
|
||||
result.Opportunity.Confidence*100))
|
||||
default:
|
||||
engine.logger.Warn("Opportunity channel full, dropping opportunity")
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(5 * time.Second):
|
||||
engine.logger.Warn("Scan task timed out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processScanTask processes a single scan task
|
||||
func (engine *ArbitrageDetectionEngine) processScanTask(task ScanTask) {
|
||||
start := time.Now()
|
||||
|
||||
// Find arbitrage paths between exchanges
|
||||
paths := engine.findArbitragePaths(task.TokenPair, task.Exchanges)
|
||||
|
||||
var bestOpportunity *types.ArbitrageOpportunity
|
||||
|
||||
for _, path := range paths {
|
||||
// Calculate arbitrage opportunity
|
||||
opportunity, err := engine.calculator.CalculateArbitrageOpportunity(
|
||||
path,
|
||||
task.InputAmount,
|
||||
math.TokenInfo{
|
||||
Address: task.TokenPair.Token0.Address,
|
||||
Symbol: task.TokenPair.Token0.Symbol,
|
||||
Decimals: task.TokenPair.Token0.Decimals,
|
||||
},
|
||||
math.TokenInfo{
|
||||
Address: task.TokenPair.Token1.Address,
|
||||
Symbol: task.TokenPair.Token1.Symbol,
|
||||
Decimals: task.TokenPair.Token1.Decimals,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is the best opportunity so far
|
||||
if bestOpportunity == nil || engine.isOpportunityBetter(opportunity, bestOpportunity) {
|
||||
bestOpportunity = opportunity
|
||||
}
|
||||
}
|
||||
|
||||
result := ScanResult{
|
||||
Opportunity: bestOpportunity,
|
||||
ScanTime: time.Since(start),
|
||||
}
|
||||
|
||||
task.ResultChan <- result
|
||||
}
|
||||
|
||||
// findArbitragePaths finds possible arbitrage paths between exchanges
|
||||
func (engine *ArbitrageDetectionEngine) findArbitragePaths(pair exchanges.TokenPair, exchangeConfigs []*exchanges.ExchangeConfig) [][]*math.PoolData {
|
||||
paths := make([][]*math.PoolData, 0)
|
||||
|
||||
// For simplicity, we'll focus on 2-hop arbitrage (buy on exchange A, sell on exchange B)
|
||||
// Production implementation would include multi-hop paths
|
||||
|
||||
token0Addr := common.HexToAddress(pair.Token0.Address)
|
||||
token1Addr := common.HexToAddress(pair.Token1.Address)
|
||||
|
||||
for i, exchange1 := range exchangeConfigs {
|
||||
for j, exchange2 := range exchangeConfigs {
|
||||
if i == j {
|
||||
continue // Same exchange
|
||||
}
|
||||
|
||||
// Find pools on each exchange
|
||||
pool1 := engine.findBestPool(exchange1, token0Addr, token1Addr)
|
||||
pool2 := engine.findBestPool(exchange2, token1Addr, token0Addr) // Reverse direction
|
||||
|
||||
if pool1 != nil && pool2 != nil {
|
||||
path := []*math.PoolData{pool1, pool2}
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// findBestPool finds the best pool for a token pair on an exchange
|
||||
func (engine *ArbitrageDetectionEngine) findBestPool(exchange *exchanges.ExchangeConfig, token0, token1 common.Address) *math.PoolData {
|
||||
// Get the pool detector and liquidity fetcher from the registry
|
||||
poolDetector := engine.registry.GetPoolDetector(exchange.Type)
|
||||
liquidityFetcher := engine.registry.GetLiquidityFetcher(exchange.Type)
|
||||
|
||||
if poolDetector == nil || liquidityFetcher == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get pools for this pair
|
||||
pools, err := poolDetector.GetAllPools(token0, token1)
|
||||
if err != nil || len(pools) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For now, return data for the first pool
|
||||
// Production implementation would compare liquidity and select the best
|
||||
poolData, err := liquidityFetcher.GetPoolData(pools[0])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return poolData
|
||||
}
|
||||
|
||||
// isOpportunityBetter compares two opportunities and returns true if the first is better
|
||||
func (engine *ArbitrageDetectionEngine) isOpportunityBetter(opp1, opp2 *types.ArbitrageOpportunity) bool {
|
||||
if opp1 == nil {
|
||||
return false
|
||||
}
|
||||
if opp2 == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if opp1.Quantities != nil && opp2.Quantities != nil {
|
||||
net1, err1 := engine.decimalAmountToUniversal(opp1.Quantities.NetProfit)
|
||||
net2, err2 := engine.decimalAmountToUniversal(opp2.Quantities.NetProfit)
|
||||
if err1 == nil && err2 == nil {
|
||||
cmp, err := engine.decimalConverter.Compare(net1, net2)
|
||||
if err == nil {
|
||||
if cmp > 0 {
|
||||
return true
|
||||
} else if cmp < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to canonical big.Int comparison
|
||||
if opp1.NetProfit != nil && opp2.NetProfit != nil {
|
||||
if opp1.NetProfit.Cmp(opp2.NetProfit) > 0 {
|
||||
return true
|
||||
} else if opp1.NetProfit.Cmp(opp2.NetProfit) < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return opp1.Confidence > opp2.Confidence
|
||||
}
|
||||
|
||||
// processPathTask processes a path finding task
|
||||
func (engine *ArbitrageDetectionEngine) processPathTask(task ScanTask) {
|
||||
// This would be used for more complex path finding algorithms
|
||||
// For now, defer to the main scan task processing
|
||||
engine.processScanTask(task)
|
||||
}
|
||||
|
||||
func (engine *ArbitrageDetectionEngine) decimalAmountToUniversal(dec types.DecimalAmount) (*math.UniversalDecimal, error) {
|
||||
if dec.Value == "" {
|
||||
return nil, fmt.Errorf("decimal amount empty")
|
||||
}
|
||||
value, ok := new(big.Int).SetString(dec.Value, 10)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid decimal amount %s", dec.Value)
|
||||
}
|
||||
return math.NewUniversalDecimal(value, dec.Decimals, dec.Symbol)
|
||||
}
|
||||
|
||||
// opportunityProcessor processes discovered opportunities
|
||||
func (engine *ArbitrageDetectionEngine) opportunityProcessor(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-engine.stopChan:
|
||||
return
|
||||
case opportunity := <-engine.opportunityChan:
|
||||
engine.processOpportunity(opportunity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processOpportunity handles the detailed processing of a discovered arbitrage opportunity.
|
||||
// This includes logging detailed information about the opportunity, validating its parameters,
|
||||
// and potentially forwarding it to execution systems.
|
||||
//
|
||||
// The processing includes:
|
||||
// 1. Detailed logging of opportunity parameters
|
||||
// 2. Validation of profit calculations
|
||||
// 3. Risk assessment
|
||||
// 4. Forwarding to registered opportunity handlers
|
||||
//
|
||||
// Parameters:
|
||||
// - opportunity: The arbitrage opportunity to process
|
||||
func (engine *ArbitrageDetectionEngine) processOpportunity(opportunity *types.ArbitrageOpportunity) {
|
||||
// Log the opportunity discovery with truncated addresses for readability
|
||||
engine.logger.Info(fmt.Sprintf("Processing arbitrage opportunity: %s -> %s",
|
||||
opportunity.TokenIn.Hex()[:8],
|
||||
opportunity.TokenOut.Hex()[:8]))
|
||||
|
||||
if opportunity.Quantities != nil {
|
||||
if amt, err := engine.decimalAmountToUniversal(opportunity.Quantities.AmountIn); err == nil {
|
||||
engine.logger.Info(fmt.Sprintf(" Input Amount: %s %s",
|
||||
engine.decimalConverter.ToHumanReadable(amt), amt.Symbol))
|
||||
}
|
||||
} else if opportunity.AmountIn != nil {
|
||||
amountDisplay := ethAmountString(engine.decimalConverter, nil, opportunity.AmountIn)
|
||||
engine.logger.Info(fmt.Sprintf(" Input Amount: %s", amountDisplay))
|
||||
}
|
||||
|
||||
engine.logger.Info(fmt.Sprintf(" Input Token: %s",
|
||||
opportunity.TokenIn.Hex()))
|
||||
|
||||
if opportunity.Quantities != nil {
|
||||
if net, err := engine.decimalAmountToUniversal(opportunity.Quantities.NetProfit); err == nil {
|
||||
engine.logger.Info(fmt.Sprintf(" Net Profit: %s %s",
|
||||
engine.decimalConverter.ToHumanReadable(net), net.Symbol))
|
||||
}
|
||||
} else if opportunity.NetProfit != nil {
|
||||
netProfitDisplay := ethAmountString(engine.decimalConverter, nil, opportunity.NetProfit)
|
||||
engine.logger.Info(fmt.Sprintf(" Net Profit: %s ETH",
|
||||
netProfitDisplay))
|
||||
}
|
||||
|
||||
engine.logger.Info(fmt.Sprintf(" ROI: %.2f%%", opportunity.ROI))
|
||||
|
||||
if opportunity.Quantities != nil {
|
||||
if impact, err := engine.decimalAmountToUniversal(opportunity.Quantities.PriceImpact); err == nil {
|
||||
engine.logger.Info(fmt.Sprintf(" Price Impact: %s %s",
|
||||
engine.decimalConverter.ToHumanReadable(impact), impact.Symbol))
|
||||
}
|
||||
} else {
|
||||
engine.logger.Info(fmt.Sprintf(" Price Impact: %.2f%%", opportunity.PriceImpact))
|
||||
}
|
||||
|
||||
engine.logger.Info(fmt.Sprintf(" Confidence: %.1f%%", opportunity.Confidence*100))
|
||||
|
||||
engine.logger.Info(fmt.Sprintf(" Risk Level: %.2f", opportunity.Risk))
|
||||
|
||||
engine.logger.Info(fmt.Sprintf(" Protocol: %s", opportunity.Protocol))
|
||||
engine.logger.Info(fmt.Sprintf(" Path length: %d", len(opportunity.Path)))
|
||||
|
||||
if engine.opportunityHandler != nil {
|
||||
// CRITICAL FIX: Use semaphore for backpressure to prevent OOM
|
||||
// Bug: Unbounded goroutine creation under high opportunity rate
|
||||
// Fix: Acquire semaphore before launching handler goroutine
|
||||
select {
|
||||
case engine.handlerSemaphore <- struct{}{}:
|
||||
// Successfully acquired semaphore slot
|
||||
go func(opp *types.ArbitrageOpportunity) {
|
||||
defer func() {
|
||||
<-engine.handlerSemaphore // Release semaphore
|
||||
}()
|
||||
engine.opportunityHandler(opp)
|
||||
}(opportunity)
|
||||
default:
|
||||
// All handler slots busy - log and drop opportunity
|
||||
engine.logger.Warn(fmt.Sprintf("Handler backpressure: dropping opportunity (all %d handlers busy)", engine.maxHandlers))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetOpportunityHandler registers a callback function that will be invoked when a profitable
|
||||
// arbitrage opportunity is discovered. This is the primary integration point between the
|
||||
// detection engine and execution systems.
|
||||
//
|
||||
// The handler function should:
|
||||
// 1. Perform additional validation if needed
|
||||
// 2. Make final go/no-go decisions based on current market conditions
|
||||
// 3. Trigger transaction execution if appropriate
|
||||
// 4. Handle any execution errors or failures
|
||||
//
|
||||
// Note: The handler is called asynchronously to avoid blocking the detection loop.
|
||||
//
|
||||
// Parameters:
|
||||
// - handler: Function to call when opportunities are discovered
|
||||
func (engine *ArbitrageDetectionEngine) SetOpportunityHandler(handler func(*types.ArbitrageOpportunity)) {
|
||||
engine.opportunityHandler = handler
|
||||
}
|
||||
|
||||
// GetOpportunityChannel returns the channel for receiving opportunities
|
||||
func (engine *ArbitrageDetectionEngine) GetOpportunityChannel() <-chan *types.ArbitrageOpportunity {
|
||||
return engine.opportunityChan
|
||||
}
|
||||
|
||||
// GetStats returns detection engine statistics
|
||||
func (engine *ArbitrageDetectionEngine) GetStats() DetectionStats {
|
||||
engine.runningMutex.RLock()
|
||||
defer engine.runningMutex.RUnlock()
|
||||
|
||||
return DetectionStats{
|
||||
IsRunning: engine.isRunning,
|
||||
TotalScans: engine.scanCount,
|
||||
OpportunitiesFound: engine.opportunityCount,
|
||||
LastScanTime: engine.lastScanTime,
|
||||
ScanInterval: engine.config.ScanInterval,
|
||||
ConfiguredExchanges: len(engine.config.EnabledExchanges),
|
||||
}
|
||||
}
|
||||
|
||||
// ScanOpportunities scans for arbitrage opportunities using the provided parameters
|
||||
func (engine *ArbitrageDetectionEngine) ScanOpportunities(ctx context.Context, params []*DetectionParams) ([]*types.ArbitrageOpportunity, error) {
|
||||
if !engine.isRunning {
|
||||
return nil, fmt.Errorf("detection engine is not running, call Start() first")
|
||||
}
|
||||
|
||||
var opportunities []*types.ArbitrageOpportunity
|
||||
|
||||
// Process each detection parameter
|
||||
for _, param := range params {
|
||||
paramOpportunities := engine.scanForSingleParam(param)
|
||||
opportunities = append(opportunities, paramOpportunities...)
|
||||
}
|
||||
|
||||
return opportunities, nil
|
||||
}
|
||||
|
||||
// scanForSingleParam handles scanning for a single detection parameter
|
||||
func (engine *ArbitrageDetectionEngine) scanForSingleParam(param *DetectionParams) []*types.ArbitrageOpportunity {
|
||||
tokenPair := engine.createTokenPair(param)
|
||||
|
||||
// Get exchange configurations for this token pair
|
||||
exchangeConfigs := engine.registry.GetExchangesForPair(common.HexToAddress(tokenPair.Token0.Address), common.HexToAddress(tokenPair.Token1.Address))
|
||||
if len(exchangeConfigs) < 2 {
|
||||
return nil // Need at least 2 exchanges for arbitrage
|
||||
}
|
||||
|
||||
// Find all possible arbitrage paths between the tokens
|
||||
paths := engine.findArbitragePaths(tokenPair, exchangeConfigs)
|
||||
|
||||
return engine.processPathsForOpportunities(paths, param)
|
||||
}
|
||||
|
||||
// createTokenPair creates a token pair from detection parameters
|
||||
func (engine *ArbitrageDetectionEngine) createTokenPair(param *DetectionParams) exchanges.TokenPair {
|
||||
// Create token info using simplified approach for now
|
||||
// In production, this would query contract metadata
|
||||
token0Info := exchanges.TokenInfo{
|
||||
Address: param.TokenA.Hex(),
|
||||
Symbol: param.TokenA.Hex()[:8], // Use first 8 chars of address as symbol
|
||||
Name: "Unknown Token",
|
||||
Decimals: 18, // Standard ERC-20 decimals
|
||||
}
|
||||
|
||||
token1Info := exchanges.TokenInfo{
|
||||
Address: param.TokenB.Hex(),
|
||||
Symbol: param.TokenB.Hex()[:8], // Use first 8 chars of address as symbol
|
||||
Name: "Unknown Token",
|
||||
Decimals: 18, // Standard ERC-20 decimals
|
||||
}
|
||||
|
||||
return exchanges.TokenPair{
|
||||
Token0: token0Info,
|
||||
Token1: token1Info,
|
||||
}
|
||||
}
|
||||
|
||||
// processPathsForOpportunities processes paths to find profitable opportunities
|
||||
func (engine *ArbitrageDetectionEngine) processPathsForOpportunities(paths [][]*math.PoolData, param *DetectionParams) []*types.ArbitrageOpportunity {
|
||||
var opportunities []*types.ArbitrageOpportunity
|
||||
|
||||
for _, path := range paths {
|
||||
if len(path) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pathOpportunities := engine.evaluatePath(path, param)
|
||||
opportunities = append(opportunities, pathOpportunities...)
|
||||
}
|
||||
|
||||
return opportunities
|
||||
}
|
||||
|
||||
// evaluatePath evaluates an arbitrage path for opportunities
|
||||
func (engine *ArbitrageDetectionEngine) evaluatePath(path []*math.PoolData, param *DetectionParams) []*types.ArbitrageOpportunity {
|
||||
var opportunities []*types.ArbitrageOpportunity
|
||||
|
||||
// Get token info for the first and last pools in the path
|
||||
tokenA := path[0].Token0
|
||||
tokenZ := path[len(path)-1].Token1
|
||||
if path[len(path)-1].Token0.Address == tokenA.Address {
|
||||
tokenZ = path[len(path)-1].Token0
|
||||
}
|
||||
|
||||
// Test various input amounts to find the most profitable one
|
||||
inputAmounts := engine.getInputAmountsToTest()
|
||||
for _, inputAmount := range inputAmounts {
|
||||
// Calculate arbitrage opportunity using the calculator
|
||||
opportunity, err := engine.calculator.CalculateArbitrageOpportunity(path, inputAmount, tokenA, tokenZ)
|
||||
if err != nil {
|
||||
engine.logger.Debug(fmt.Sprintf("Failed to calculate opportunity for path: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply filters based on the parameters
|
||||
if opportunity.NetProfit.Cmp(param.MinProfit) < 0 {
|
||||
continue // Below minimum profit threshold
|
||||
}
|
||||
|
||||
// Check slippage threshold
|
||||
if opportunity.PriceImpact > param.MaxSlippage {
|
||||
continue // Above maximum slippage tolerance
|
||||
}
|
||||
|
||||
// Add to opportunities if it passes all checks
|
||||
opportunities = append(opportunities, opportunity)
|
||||
|
||||
// For now, break after finding one good opportunity per path
|
||||
// to avoid too many similar results (can be made configurable)
|
||||
break
|
||||
}
|
||||
|
||||
return opportunities
|
||||
}
|
||||
|
||||
// DetectionStats contains statistics about the detection engine
|
||||
type DetectionStats struct {
|
||||
IsRunning bool
|
||||
TotalScans uint64
|
||||
OpportunitiesFound uint64
|
||||
LastScanTime time.Time
|
||||
ScanInterval time.Duration
|
||||
ConfiguredExchanges int
|
||||
}
|
||||
|
||||
// NewWorkerPool creates a new worker pool
|
||||
func NewWorkerPool(workers int, ctx context.Context) *WorkerPool {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
return &WorkerPool{
|
||||
workers: workers,
|
||||
taskChan: make(chan ScanTask, workers*2), // Buffered channel
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the worker pool
|
||||
func (wp *WorkerPool) Start(taskProcessor func(ScanTask)) {
|
||||
for i := 0; i < wp.workers; i++ {
|
||||
wp.wg.Add(1)
|
||||
go func() {
|
||||
defer wp.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-wp.ctx.Done():
|
||||
return
|
||||
case task := <-wp.taskChan:
|
||||
taskProcessor(task)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the worker pool
|
||||
func (wp *WorkerPool) Stop() {
|
||||
wp.cancel()
|
||||
close(wp.taskChan)
|
||||
wp.wg.Wait()
|
||||
}
|
||||
1642
orig/pkg/arbitrage/executor.go
Normal file
1642
orig/pkg/arbitrage/executor.go
Normal file
File diff suppressed because it is too large
Load Diff
1463
orig/pkg/arbitrage/flash_executor.go
Normal file
1463
orig/pkg/arbitrage/flash_executor.go
Normal file
File diff suppressed because it is too large
Load Diff
205
orig/pkg/arbitrage/flash_executor_test.go
Normal file
205
orig/pkg/arbitrage/flash_executor_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
|
||||
"github.com/fraktal/mev-beta/bindings/contracts"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
type mockArbitrageLogParser struct {
|
||||
event *contracts.ArbitrageExecutorArbitrageExecuted
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockArbitrageLogParser) ParseArbitrageExecuted(types.Log) (*contracts.ArbitrageExecutorArbitrageExecuted, error) {
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
return m.event, nil
|
||||
}
|
||||
|
||||
func newTestLogger() *logger.Logger {
|
||||
return logger.New("error", "text", "")
|
||||
}
|
||||
|
||||
func TestCalculateActualProfit_UsesArbitrageEvent(t *testing.T) {
|
||||
arbitrageAddr := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
||||
executor := NewFlashSwapExecutor(nil, newTestLogger(), nil, nil, common.Address{}, arbitrageAddr, ExecutionConfig{})
|
||||
|
||||
profit := new(big.Int).Mul(big.NewInt(2), powerOfTenInt(18))
|
||||
gasPrice := big.NewInt(1_000_000_000) // 1 gwei
|
||||
receipt := &types.Receipt{
|
||||
Logs: []*types.Log{{Address: arbitrageAddr}},
|
||||
GasUsed: 100000,
|
||||
EffectiveGasPrice: gasPrice,
|
||||
}
|
||||
|
||||
event := &contracts.ArbitrageExecutorArbitrageExecuted{
|
||||
Tokens: []common.Address{executor.ethReferenceToken},
|
||||
Amounts: []*big.Int{profit},
|
||||
Profit: profit,
|
||||
}
|
||||
|
||||
executor.arbitrageBinding = &mockArbitrageLogParser{event: event}
|
||||
|
||||
opportunity := &pkgtypes.ArbitrageOpportunity{
|
||||
TokenOut: executor.ethReferenceToken,
|
||||
Quantities: &pkgtypes.OpportunityQuantities{
|
||||
NetProfit: pkgtypes.DecimalAmount{Symbol: "WETH", Decimals: 18},
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := executor.calculateActualProfit(receipt, opportunity)
|
||||
if err != nil {
|
||||
t.Fatalf("calculateActualProfit returned error: %v", err)
|
||||
}
|
||||
|
||||
gasCost := new(big.Int).Mul(big.NewInt(0).SetUint64(receipt.GasUsed), gasPrice)
|
||||
expected := new(big.Int).Sub(profit, gasCost)
|
||||
|
||||
if actual.Value.Cmp(expected) != 0 {
|
||||
t.Fatalf("expected profit %s, got %s", expected.String(), actual.Value.String())
|
||||
}
|
||||
|
||||
if actual.Decimals != 18 {
|
||||
t.Fatalf("expected decimals 18, got %d", actual.Decimals)
|
||||
}
|
||||
|
||||
if actual.Symbol != "WETH" {
|
||||
t.Fatalf("expected symbol WETH, got %s", actual.Symbol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateActualProfit_FallbackToOpportunity(t *testing.T) {
|
||||
arbitrageAddr := common.HexToAddress("0x1234567890123456789012345678901234567891")
|
||||
executor := NewFlashSwapExecutor(nil, newTestLogger(), nil, nil, common.Address{}, arbitrageAddr, ExecutionConfig{})
|
||||
|
||||
profit := big.NewInt(1_500_000) // 1.5 USDC with 6 decimals
|
||||
gasPrice := big.NewInt(1_000_000_000) // 1 gwei
|
||||
receipt := &types.Receipt{
|
||||
Logs: []*types.Log{{Address: arbitrageAddr}},
|
||||
GasUsed: 100000,
|
||||
EffectiveGasPrice: gasPrice,
|
||||
}
|
||||
|
||||
opportunity := &pkgtypes.ArbitrageOpportunity{
|
||||
TokenOut: common.HexToAddress("0xaF88d065e77c8cC2239327C5EDb3A432268e5831"), // USDC
|
||||
NetProfit: profit,
|
||||
Quantities: &pkgtypes.OpportunityQuantities{
|
||||
NetProfit: pkgtypes.DecimalAmount{Symbol: "USDC", Decimals: 6},
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := executor.calculateActualProfit(receipt, opportunity)
|
||||
if err != nil {
|
||||
t.Fatalf("calculateActualProfit returned error: %v", err)
|
||||
}
|
||||
|
||||
gasCostEth := new(big.Int).Mul(big.NewInt(0).SetUint64(receipt.GasUsed), gasPrice)
|
||||
// Gas cost conversion: 0.0001 ETH * 2000 USD / 1 USD = 0.2 USDC => 200000 units
|
||||
gasCostUSDC := big.NewInt(200000)
|
||||
expected := new(big.Int).Sub(profit, gasCostUSDC)
|
||||
|
||||
if actual.Value.Cmp(expected) != 0 {
|
||||
t.Fatalf("expected profit %s, got %s", expected.String(), actual.Value.String())
|
||||
}
|
||||
|
||||
if actual.Decimals != 6 {
|
||||
t.Fatalf("expected decimals 6, got %d", actual.Decimals)
|
||||
}
|
||||
|
||||
if actual.Symbol != "USDC" {
|
||||
t.Fatalf("expected symbol USDC, got %s", actual.Symbol)
|
||||
}
|
||||
|
||||
// Ensure ETH gas cost unchanged for reference
|
||||
if gasCostEth.Sign() == 0 {
|
||||
t.Fatalf("expected non-zero gas cost")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateActualProfit_NoPriceData(t *testing.T) {
|
||||
arbitrageAddr := common.HexToAddress("0x1234567890123456789012345678901234567892")
|
||||
executor := NewFlashSwapExecutor(nil, newTestLogger(), nil, nil, common.Address{}, arbitrageAddr, ExecutionConfig{})
|
||||
|
||||
profit := new(big.Int).Mul(big.NewInt(3), powerOfTenInt(17)) // 0.3 units with 18 decimals
|
||||
receipt := &types.Receipt{
|
||||
Logs: []*types.Log{{Address: arbitrageAddr}},
|
||||
GasUsed: 50000,
|
||||
EffectiveGasPrice: big.NewInt(2_000_000_000),
|
||||
}
|
||||
|
||||
unknownToken := common.HexToAddress("0x9b8D58d870495459c1004C34357F3bf06c0dB0b3")
|
||||
opportunity := &pkgtypes.ArbitrageOpportunity{
|
||||
TokenOut: unknownToken,
|
||||
NetProfit: profit,
|
||||
Quantities: &pkgtypes.OpportunityQuantities{
|
||||
NetProfit: pkgtypes.DecimalAmount{Symbol: "XYZ", Decimals: 18},
|
||||
},
|
||||
}
|
||||
|
||||
actual, err := executor.calculateActualProfit(receipt, opportunity)
|
||||
if err != nil {
|
||||
t.Fatalf("calculateActualProfit returned error: %v", err)
|
||||
}
|
||||
|
||||
if actual.Value.Cmp(profit) != 0 {
|
||||
t.Fatalf("expected profit %s, got %s", profit.String(), actual.Value.String())
|
||||
}
|
||||
|
||||
if actual.Symbol != "XYZ" {
|
||||
t.Fatalf("expected symbol XYZ, got %s", actual.Symbol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRevertReason_ErrorString(t *testing.T) {
|
||||
strType, err := abi.NewType("string", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create ABI type: %v", err)
|
||||
}
|
||||
|
||||
args := abi.Arguments{{Type: strType}}
|
||||
payload, err := args.Pack("execution reverted: slippage limit")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to pack revert reason: %v", err)
|
||||
}
|
||||
|
||||
data := append([]byte{0x08, 0xc3, 0x79, 0xa0}, payload...)
|
||||
reason := parseRevertReason(data)
|
||||
if reason != "execution reverted: slippage limit" {
|
||||
t.Fatalf("expected revert reason, got %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRevertReason_PanicCode(t *testing.T) {
|
||||
uintType, err := abi.NewType("uint256", "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create uint256 ABI type: %v", err)
|
||||
}
|
||||
|
||||
args := abi.Arguments{{Type: uintType}}
|
||||
payload, err := args.Pack(big.NewInt(0x41))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to pack panic code: %v", err)
|
||||
}
|
||||
|
||||
data := append([]byte{0x4e, 0x48, 0x7b, 0x71}, payload...)
|
||||
reason := parseRevertReason(data)
|
||||
if reason != "panic code 0x41" {
|
||||
t.Fatalf("expected panic code, got %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRevertReason_Unknown(t *testing.T) {
|
||||
reason := parseRevertReason([]byte{0x00, 0x01, 0x02, 0x03})
|
||||
if reason != "" {
|
||||
t.Fatalf("expected empty reason, got %q", reason)
|
||||
}
|
||||
}
|
||||
38
orig/pkg/arbitrage/flash_swap_utils.go
Normal file
38
orig/pkg/arbitrage/flash_swap_utils.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
type flashSwapCallback struct {
|
||||
TokenPath []common.Address
|
||||
PoolPath []common.Address
|
||||
Fees []*big.Int
|
||||
MinAmountOut *big.Int
|
||||
}
|
||||
|
||||
func encodeFlashSwapCallback(tokenPath []common.Address, poolPath []common.Address, fees []*big.Int, minAmountOut *big.Int) ([]byte, error) {
|
||||
tupleType, err := abi.NewType("tuple", "flashSwapCallback", []abi.ArgumentMarshaling{
|
||||
{Name: "tokenPath", Type: "address[]"},
|
||||
{Name: "poolPath", Type: "address[]"},
|
||||
{Name: "fees", Type: "uint256[]"},
|
||||
{Name: "minAmountOut", Type: "uint256"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arguments := abi.Arguments{{Type: tupleType}}
|
||||
|
||||
callback := flashSwapCallback{
|
||||
TokenPath: tokenPath,
|
||||
PoolPath: poolPath,
|
||||
Fees: fees,
|
||||
MinAmountOut: minAmountOut,
|
||||
}
|
||||
|
||||
return arguments.Pack(callback)
|
||||
}
|
||||
1005
orig/pkg/arbitrage/live_execution_framework.go
Normal file
1005
orig/pkg/arbitrage/live_execution_framework.go
Normal file
File diff suppressed because it is too large
Load Diff
108
orig/pkg/arbitrage/live_execution_framework_test.go
Normal file
108
orig/pkg/arbitrage/live_execution_framework_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
func TestPerformRiskChecksRespectsDailyLossLimitDecimals(t *testing.T) {
|
||||
log := logger.New("debug", "text", "")
|
||||
dc := math.NewDecimalConverter()
|
||||
|
||||
maxPos, _ := dc.FromString("10", 18, "ETH")
|
||||
dailyLimit, _ := dc.FromString("0.5", 18, "ETH")
|
||||
profitTarget, _ := dc.FromString("1", 18, "ETH")
|
||||
zero, _ := dc.FromString("0", 18, "ETH")
|
||||
|
||||
framework := &LiveExecutionFramework{
|
||||
logger: log,
|
||||
decimalConverter: dc,
|
||||
config: FrameworkConfig{
|
||||
MaxPositionSize: maxPos,
|
||||
DailyLossLimit: dailyLimit,
|
||||
DailyProfitTarget: profitTarget,
|
||||
},
|
||||
stats: &FrameworkStats{
|
||||
DailyStats: make(map[string]*DailyStats),
|
||||
},
|
||||
}
|
||||
|
||||
today := time.Now().Format("2006-01-02")
|
||||
framework.stats.DailyStats[today] = &DailyStats{
|
||||
Date: today,
|
||||
ProfitRealized: zero.Copy(),
|
||||
GasCostPaid: zero.Copy(),
|
||||
}
|
||||
|
||||
loss, _ := math.NewUniversalDecimal(big.NewInt(-600000000000000000), 18, "ETH")
|
||||
framework.stats.DailyStats[today].NetProfit = loss
|
||||
|
||||
opportunity := &pkgtypes.ArbitrageOpportunity{
|
||||
AmountIn: big.NewInt(1000000000000000000),
|
||||
Confidence: 0.95,
|
||||
}
|
||||
|
||||
if framework.performRiskChecks(opportunity) {
|
||||
t.Fatalf("expected opportunity to be rejected when loss exceeds limit")
|
||||
}
|
||||
|
||||
acceptableLoss, _ := math.NewUniversalDecimal(big.NewInt(-200000000000000000), 18, "ETH")
|
||||
framework.stats.DailyStats[today].NetProfit = acceptableLoss
|
||||
|
||||
if !framework.performRiskChecks(opportunity) {
|
||||
t.Fatalf("expected opportunity to pass when loss within limit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpportunityHelpersHandleMissingQuantities(t *testing.T) {
|
||||
dc := math.NewDecimalConverter()
|
||||
profit := big.NewInt(2100000000000000)
|
||||
opportunity := &pkgtypes.ArbitrageOpportunity{
|
||||
NetProfit: profit,
|
||||
}
|
||||
|
||||
expectedDecimal := universalFromWei(dc, profit, "ETH")
|
||||
|
||||
resultDecimal := opportunityNetProfitDecimal(dc, opportunity)
|
||||
if cmp, err := dc.Compare(resultDecimal, expectedDecimal); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected fallback decimal to match wei amount, got %s", dc.ToHumanReadable(resultDecimal))
|
||||
}
|
||||
|
||||
expectedString := ethAmountString(dc, expectedDecimal, nil)
|
||||
resultString := opportunityAmountString(dc, opportunity)
|
||||
if resultString != expectedString {
|
||||
t.Fatalf("expected fallback string %s, got %s", expectedString, resultString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpportunityHelpersPreferDecimalSnapshots(t *testing.T) {
|
||||
dc := math.NewDecimalConverter()
|
||||
declared, _ := math.NewUniversalDecimal(big.NewInt(1500000000000000000), 18, "ETH")
|
||||
|
||||
opportunity := &pkgtypes.ArbitrageOpportunity{
|
||||
NetProfit: new(big.Int).Set(declared.Value),
|
||||
Quantities: &pkgtypes.OpportunityQuantities{
|
||||
NetProfit: pkgtypes.DecimalAmount{
|
||||
Value: declared.Value.String(),
|
||||
Decimals: declared.Decimals,
|
||||
Symbol: declared.Symbol,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resultDecimal := opportunityNetProfitDecimal(dc, opportunity)
|
||||
if cmp, err := dc.Compare(resultDecimal, declared); err != nil || cmp != 0 {
|
||||
t.Fatalf("expected decimal snapshot to be used, got %s", dc.ToHumanReadable(resultDecimal))
|
||||
}
|
||||
|
||||
expectedString := ethAmountString(dc, declared, nil)
|
||||
resultString := opportunityAmountString(dc, opportunity)
|
||||
if resultString != expectedString {
|
||||
t.Fatalf("expected human-readable snapshot %s, got %s", expectedString, resultString)
|
||||
}
|
||||
}
|
||||
1082
orig/pkg/arbitrage/multihop.go
Normal file
1082
orig/pkg/arbitrage/multihop.go
Normal file
File diff suppressed because it is too large
Load Diff
436
orig/pkg/arbitrage/multihop_test.go
Normal file
436
orig/pkg/arbitrage/multihop_test.go
Normal file
@@ -0,0 +1,436 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/pkg/market"
|
||||
)
|
||||
|
||||
// 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, nil, marketMgr)
|
||||
|
||||
assert.NotNil(t, scanner)
|
||||
assert.Equal(t, log, scanner.logger)
|
||||
// Note: marketMgr is not stored in the scanner struct
|
||||
// NOTE: These values have been optimized for aggressive opportunity detection:
|
||||
// - maxHops reduced from 4 to 3 for faster execution
|
||||
// - minProfitWei reduced to 0.00001 ETH for more opportunities
|
||||
// - maxSlippage increased to 5% for broader market coverage
|
||||
// - maxPaths increased to 200 for thorough opportunity search
|
||||
// - pathTimeout increased to 2s for complete analysis
|
||||
assert.Equal(t, 3, scanner.maxHops)
|
||||
assert.Equal(t, "10000000000000", scanner.minProfitWei.String())
|
||||
assert.Equal(t, 0.05, scanner.maxSlippage)
|
||||
assert.Equal(t, 200, scanner.maxPaths)
|
||||
assert.Equal(t, time.Second*2, 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")
|
||||
sqrtPriceX96, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
pool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: tokenA,
|
||||
Token1: tokenB,
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000),
|
||||
SqrtPriceX96: sqrtPriceX96,
|
||||
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, nil, marketMgr)
|
||||
|
||||
// Test usable pool (recent and sufficient liquidity)
|
||||
now := time.Now()
|
||||
sqrtPriceX961, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
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: sqrtPriceX961,
|
||||
LastUpdated: now,
|
||||
}
|
||||
|
||||
assert.True(t, scanner.isPoolUsable(usablePool))
|
||||
|
||||
// Test pool with insufficient liquidity
|
||||
sqrtPriceX962, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
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: sqrtPriceX962,
|
||||
LastUpdated: now,
|
||||
}
|
||||
|
||||
assert.False(t, scanner.isPoolUsable(unusablePool1))
|
||||
|
||||
// Test stale pool
|
||||
sqrtPriceX963, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
stalePool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x3"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: sqrtPriceX963,
|
||||
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, nil, 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)
|
||||
sqrtPriceX965, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
pool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: tokenIn,
|
||||
Token1: tokenOut,
|
||||
Protocol: "UniswapV2",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: sqrtPriceX965,
|
||||
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, nil, marketMgr)
|
||||
|
||||
// Create a pool with known values for testing
|
||||
tokenIn := common.HexToAddress("0xA")
|
||||
tokenOut := common.HexToAddress("0xB")
|
||||
|
||||
// Create a pool with realistic values
|
||||
sqrtPriceX96, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
pool := &PoolInfo{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: tokenIn,
|
||||
Token1: tokenOut,
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: sqrtPriceX96,
|
||||
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, nil, marketMgr)
|
||||
|
||||
// NOTE: Gas estimates have been optimized for flash loan execution:
|
||||
// Flash loans are more efficient than capital-requiring swaps because:
|
||||
// - No capital lock-up required
|
||||
// - Lower slippage on large amounts
|
||||
// - More predictable execution
|
||||
// Therefore, gas costs are realistically lower than non-flash-loan swaps
|
||||
|
||||
// Test UniswapV3 - optimized to 70k for flash loans
|
||||
gas := scanner.estimateHopGasCost("UniswapV3")
|
||||
assert.Equal(t, int64(70000), gas.Int64())
|
||||
|
||||
// Test UniswapV2 - optimized to 60k for flash loans
|
||||
gas = scanner.estimateHopGasCost("UniswapV2")
|
||||
assert.Equal(t, int64(60000), gas.Int64())
|
||||
|
||||
// Test SushiSwap - optimized to 60k for flash loans (similar to V2)
|
||||
gas = scanner.estimateHopGasCost("SushiSwap")
|
||||
assert.Equal(t, int64(60000), gas.Int64())
|
||||
|
||||
// Test default case - conservative estimate of 70k
|
||||
gas = scanner.estimateHopGasCost("UnknownProtocol")
|
||||
assert.Equal(t, int64(70000), gas.Int64())
|
||||
}
|
||||
|
||||
// TestIsProfitable tests the isProfitable function
|
||||
func TestIsProfitable(t *testing.T) {
|
||||
log := logger.New("info", "text", "")
|
||||
marketMgr := &market.MarketManager{}
|
||||
scanner := NewMultiHopScanner(log, nil, 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, nil, marketMgr)
|
||||
|
||||
// Test with invalid inputs
|
||||
tokens := []common.Address{
|
||||
common.HexToAddress("0xA"),
|
||||
common.HexToAddress("0xB"),
|
||||
}
|
||||
sqrtPriceX966, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
pools := []*PoolInfo{
|
||||
{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: sqrtPriceX966,
|
||||
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
|
||||
}
|
||||
sqrtPriceX967, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
sqrtPriceX968, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
sqrtPriceX969, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
validPools := []*PoolInfo{
|
||||
{
|
||||
Address: common.HexToAddress("0x1"),
|
||||
Token0: common.HexToAddress("0xA"),
|
||||
Token1: common.HexToAddress("0xB"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: sqrtPriceX967,
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("0x2"),
|
||||
Token0: common.HexToAddress("0xB"),
|
||||
Token1: common.HexToAddress("0xC"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: sqrtPriceX968,
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
{
|
||||
Address: common.HexToAddress("0x3"),
|
||||
Token0: common.HexToAddress("0xC"),
|
||||
Token1: common.HexToAddress("0xA"),
|
||||
Protocol: "UniswapV3",
|
||||
Fee: 3000,
|
||||
Liquidity: uint256.NewInt(1000000000000000000),
|
||||
SqrtPriceX96: sqrtPriceX969,
|
||||
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{}
|
||||
sqrtPriceX9610, _ := uint256.FromDecimal("79228162514264337593543950336")
|
||||
|
||||
// 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: sqrtPriceX9610,
|
||||
LastUpdated: time.Now(),
|
||||
},
|
||||
})
|
||||
|
||||
// Skip this test due to deadlock issues in token graph update
|
||||
t.Skip("TestScanForArbitrage skipped: deadlock in updateTokenGraph with RWMutex")
|
||||
|
||||
scanner := NewMultiHopScanner(log, nil, mockMarketMgr)
|
||||
|
||||
// Use a context with timeout to prevent the test from hanging
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
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)
|
||||
// It's perfectly valid for ScanForArbitrage to return nil or an empty slice when no arbitrage opportunities exist
|
||||
// The important thing is that it doesn't return an error
|
||||
// We're not asserting anything about the paths value since nil is acceptable in this case
|
||||
_ = paths // explicitly ignore paths to avoid 'declared and not used' error
|
||||
}
|
||||
150
orig/pkg/arbitrage/nonce_manager.go
Normal file
150
orig/pkg/arbitrage/nonce_manager.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
||||
// NonceManager provides thread-safe nonce management for transaction submission
|
||||
// Prevents nonce collisions when submitting multiple transactions rapidly
|
||||
type NonceManager struct {
|
||||
mu sync.Mutex
|
||||
|
||||
client *ethclient.Client
|
||||
account common.Address
|
||||
|
||||
// Track the last nonce we've assigned
|
||||
lastNonce uint64
|
||||
|
||||
// Track pending nonces to avoid reuse
|
||||
pending map[uint64]bool
|
||||
|
||||
// Initialized flag
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// NewNonceManager creates a new nonce manager for the given account
|
||||
func NewNonceManager(client *ethclient.Client, account common.Address) *NonceManager {
|
||||
return &NonceManager{
|
||||
client: client,
|
||||
account: account,
|
||||
pending: make(map[uint64]bool),
|
||||
initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
// GetNextNonce returns the next available nonce for transaction submission
|
||||
// This method is thread-safe and prevents nonce collisions
|
||||
func (nm *NonceManager) GetNextNonce(ctx context.Context) (uint64, error) {
|
||||
nm.mu.Lock()
|
||||
defer nm.mu.Unlock()
|
||||
|
||||
// Get current pending nonce from network
|
||||
currentNonce, err := nm.client.PendingNonceAt(ctx, nm.account)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get pending nonce: %w", err)
|
||||
}
|
||||
|
||||
// First time initialization
|
||||
if !nm.initialized {
|
||||
// On first call, hand back the network's pending nonce so we don't
|
||||
// skip a slot and create gaps that block execution.
|
||||
nm.lastNonce = currentNonce
|
||||
nm.initialized = true
|
||||
nm.pending[currentNonce] = true
|
||||
return currentNonce, nil
|
||||
}
|
||||
|
||||
// Determine next nonce to use
|
||||
var nextNonce uint64
|
||||
|
||||
// If network nonce is higher than our last assigned, use network nonce
|
||||
// This handles cases where transactions confirmed between calls
|
||||
if currentNonce > nm.lastNonce {
|
||||
nextNonce = currentNonce
|
||||
nm.lastNonce = currentNonce
|
||||
// Clear pending nonces below current (they've been mined)
|
||||
nm.clearPendingBefore(currentNonce)
|
||||
} else {
|
||||
// Otherwise increment our last nonce
|
||||
nextNonce = nm.lastNonce + 1
|
||||
nm.lastNonce = nextNonce
|
||||
}
|
||||
|
||||
// Mark this nonce as pending
|
||||
nm.pending[nextNonce] = true
|
||||
|
||||
return nextNonce, nil
|
||||
}
|
||||
|
||||
// MarkConfirmed marks a nonce as confirmed (mined in a block)
|
||||
// This allows the nonce manager to clean up its pending tracking
|
||||
func (nm *NonceManager) MarkConfirmed(nonce uint64) {
|
||||
nm.mu.Lock()
|
||||
defer nm.mu.Unlock()
|
||||
|
||||
delete(nm.pending, nonce)
|
||||
}
|
||||
|
||||
// MarkFailed marks a nonce as failed (transaction rejected)
|
||||
// This allows the nonce to be potentially reused
|
||||
func (nm *NonceManager) MarkFailed(nonce uint64) {
|
||||
nm.mu.Lock()
|
||||
defer nm.mu.Unlock()
|
||||
|
||||
delete(nm.pending, nonce)
|
||||
|
||||
// Reset lastNonce if this was the last one we assigned
|
||||
if nonce == nm.lastNonce && nonce > 0 {
|
||||
nm.lastNonce = nonce - 1
|
||||
}
|
||||
}
|
||||
|
||||
// GetPendingCount returns the number of pending nonces
|
||||
func (nm *NonceManager) GetPendingCount() int {
|
||||
nm.mu.Lock()
|
||||
defer nm.mu.Unlock()
|
||||
|
||||
return len(nm.pending)
|
||||
}
|
||||
|
||||
// Reset resets the nonce manager state
|
||||
// Should be called if you want to re-sync with network state
|
||||
func (nm *NonceManager) Reset() {
|
||||
nm.mu.Lock()
|
||||
defer nm.mu.Unlock()
|
||||
|
||||
nm.pending = make(map[uint64]bool)
|
||||
nm.initialized = false
|
||||
nm.lastNonce = 0
|
||||
}
|
||||
|
||||
// clearPendingBefore removes pending nonces below the given threshold
|
||||
// (internal method, mutex must be held by caller)
|
||||
func (nm *NonceManager) clearPendingBefore(threshold uint64) {
|
||||
for nonce := range nm.pending {
|
||||
if nonce < threshold {
|
||||
delete(nm.pending, nonce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentNonce returns the last assigned nonce without incrementing
|
||||
func (nm *NonceManager) GetCurrentNonce() uint64 {
|
||||
nm.mu.Lock()
|
||||
defer nm.mu.Unlock()
|
||||
|
||||
return nm.lastNonce
|
||||
}
|
||||
|
||||
// IsPending checks if a nonce is currently pending
|
||||
func (nm *NonceManager) IsPending(nonce uint64) bool {
|
||||
nm.mu.Lock()
|
||||
defer nm.mu.Unlock()
|
||||
|
||||
return nm.pending[nonce]
|
||||
}
|
||||
2178
orig/pkg/arbitrage/service.go
Normal file
2178
orig/pkg/arbitrage/service.go
Normal file
File diff suppressed because it is too large
Load Diff
161
orig/pkg/arbitrage/swap_parser_test.go
Normal file
161
orig/pkg/arbitrage/swap_parser_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseSignedInt256(t *testing.T) {
|
||||
sas := &ArbitrageService{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "positive value",
|
||||
input: make([]byte, 32), // All zeros = 0
|
||||
expected: "0",
|
||||
},
|
||||
{
|
||||
name: "negative value",
|
||||
input: func() []byte {
|
||||
// Create a -1 value in two's complement (all 1s)
|
||||
data := make([]byte, 32)
|
||||
for i := range data {
|
||||
data[i] = 0xFF
|
||||
}
|
||||
return data
|
||||
}(),
|
||||
expected: "-1",
|
||||
},
|
||||
{
|
||||
name: "large positive value",
|
||||
input: func() []byte {
|
||||
data := make([]byte, 32)
|
||||
data[31] = 0x01 // Small positive number
|
||||
return data
|
||||
}(),
|
||||
expected: "1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := sas.parseSignedInt256(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("parseSignedInt256() error = %v", err)
|
||||
}
|
||||
if result.String() != tt.expected {
|
||||
t.Errorf("parseSignedInt256() = %v, want %v", result.String(), tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSignedInt24(t *testing.T) {
|
||||
sas := &ArbitrageService{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected int32
|
||||
}{
|
||||
{
|
||||
name: "zero value",
|
||||
input: make([]byte, 32), // All zeros
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "positive value",
|
||||
input: func() []byte {
|
||||
data := make([]byte, 32)
|
||||
data[31] = 0x01 // 1
|
||||
return data
|
||||
}(),
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "negative value (-1)",
|
||||
input: func() []byte {
|
||||
data := make([]byte, 32)
|
||||
data[28] = 0xFF
|
||||
// Set the 24-bit value to all 1s (which is -1 in two's complement)
|
||||
data[29] = 0xFF
|
||||
data[30] = 0xFF
|
||||
data[31] = 0xFF
|
||||
return data
|
||||
}(),
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
name: "max positive int24",
|
||||
input: func() []byte {
|
||||
data := make([]byte, 32)
|
||||
// 0x7FFFFF = 8388607 (max int24)
|
||||
data[29] = 0x7F
|
||||
data[30] = 0xFF
|
||||
data[31] = 0xFF
|
||||
return data
|
||||
}(),
|
||||
expected: 8388607,
|
||||
},
|
||||
{
|
||||
name: "min negative int24",
|
||||
input: func() []byte {
|
||||
data := make([]byte, 32)
|
||||
data[28] = 0xFF
|
||||
// 0x800000 = -8388608 (min int24)
|
||||
data[29] = 0x80
|
||||
data[30] = 0x00
|
||||
data[31] = 0x00
|
||||
return data
|
||||
}(),
|
||||
expected: -8388608,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := sas.parseSignedInt24(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("parseSignedInt24() error = %v", err)
|
||||
}
|
||||
if result != tt.expected {
|
||||
t.Errorf("parseSignedInt24() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSignedInt24Errors(t *testing.T) {
|
||||
sas := &ArbitrageService{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
}{
|
||||
{
|
||||
name: "invalid length",
|
||||
input: make([]byte, 16), // Wrong length
|
||||
},
|
||||
{
|
||||
name: "out of range positive",
|
||||
input: func() []byte {
|
||||
data := make([]byte, 32)
|
||||
// Set a value > 8388607
|
||||
data[28] = 0x01 // This makes it > 24 bits
|
||||
return data
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := sas.parseSignedInt24(tt.input)
|
||||
if err == nil {
|
||||
t.Errorf("parseSignedInt24() expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
51
orig/pkg/arbitrage/types.go
Normal file
51
orig/pkg/arbitrage/types.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package arbitrage
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/fraktal/mev-beta/pkg/math"
|
||||
"github.com/fraktal/mev-beta/pkg/mev"
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
)
|
||||
|
||||
// ExecutionResult represents the result of an arbitrage execution
|
||||
type ExecutionResult struct {
|
||||
TransactionHash common.Hash
|
||||
GasUsed uint64
|
||||
GasPrice *big.Int
|
||||
GasCost *math.UniversalDecimal // Changed to UniversalDecimal for consistency with other components
|
||||
ProfitRealized *big.Int
|
||||
Success bool
|
||||
Error error
|
||||
ExecutionTime time.Duration
|
||||
Path *ArbitragePath
|
||||
ErrorMessage string // Added for compatibility
|
||||
Status string // Added for compatibility
|
||||
}
|
||||
|
||||
// ExecutionTask represents a task for execution
|
||||
type ExecutionTask struct {
|
||||
Opportunity *types.ArbitrageOpportunity
|
||||
Priority int
|
||||
Deadline time.Time
|
||||
SubmissionTime time.Time // Added for tracking when the task was submitted
|
||||
CompetitionAnalysis *mev.CompetitionData // Changed to match what the analyzer returns
|
||||
ResultChan chan *ExecutionResult // Channel to send execution result back
|
||||
}
|
||||
|
||||
// DetectionParams represents parameters for arbitrage opportunity detection
|
||||
type DetectionParams struct {
|
||||
TokenA common.Address
|
||||
TokenB common.Address
|
||||
MinProfit *big.Int
|
||||
MaxSlippage float64
|
||||
}
|
||||
|
||||
// TokenPair represents a trading pair
|
||||
type TokenPair struct {
|
||||
TokenA common.Address
|
||||
TokenB common.Address
|
||||
}
|
||||
Reference in New Issue
Block a user