removed the fucking vendor files
This commit is contained in:
@@ -1,17 +1,28 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/fraktal/mev-beta/internal/config"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/fraktal/mev-beta/internal/tokens"
|
||||
"github.com/fraktal/mev-beta/pkg/circuit"
|
||||
"github.com/fraktal/mev-beta/pkg/contracts"
|
||||
"github.com/fraktal/mev-beta/pkg/database"
|
||||
"github.com/fraktal/mev-beta/pkg/events"
|
||||
"github.com/fraktal/mev-beta/pkg/pools"
|
||||
"github.com/fraktal/mev-beta/pkg/slippage"
|
||||
"github.com/fraktal/mev-beta/pkg/trading"
|
||||
"github.com/fraktal/mev-beta/pkg/types"
|
||||
"github.com/fraktal/mev-beta/pkg/uniswap"
|
||||
"github.com/holiman/uint256"
|
||||
"golang.org/x/sync/singleflight"
|
||||
@@ -30,6 +41,9 @@ type MarketScanner struct {
|
||||
cacheTTL time.Duration
|
||||
slippageProtector *trading.SlippageProtection
|
||||
circuitBreaker *circuit.CircuitBreaker
|
||||
contractExecutor *contracts.ContractExecutor
|
||||
create2Calculator *pools.CREATE2Calculator
|
||||
database *database.Database
|
||||
}
|
||||
|
||||
// EventWorker represents a worker that processes event details
|
||||
@@ -42,7 +56,7 @@ type EventWorker struct {
|
||||
}
|
||||
|
||||
// NewMarketScanner creates a new market scanner with concurrency support
|
||||
func NewMarketScanner(cfg *config.BotConfig, logger *logger.Logger) *MarketScanner {
|
||||
func NewMarketScanner(cfg *config.BotConfig, logger *logger.Logger, contractExecutor *contracts.ContractExecutor, db *database.Database) *MarketScanner {
|
||||
scanner := &MarketScanner{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
@@ -59,6 +73,9 @@ func NewMarketScanner(cfg *config.BotConfig, logger *logger.Logger) *MarketScann
|
||||
MaxRequests: 3,
|
||||
SuccessThreshold: 2,
|
||||
}),
|
||||
contractExecutor: contractExecutor,
|
||||
create2Calculator: pools.NewCREATE2Calculator(logger),
|
||||
database: db,
|
||||
}
|
||||
|
||||
// Create workers
|
||||
@@ -152,6 +169,9 @@ func (s *MarketScanner) SubmitEvent(event events.Event) {
|
||||
func (s *MarketScanner) analyzeSwapEvent(event events.Event) {
|
||||
s.logger.Debug(fmt.Sprintf("Analyzing swap event in pool %s", event.PoolAddress))
|
||||
|
||||
// Log the swap event to database
|
||||
s.logSwapEvent(event)
|
||||
|
||||
// Get pool data with caching
|
||||
poolData, err := s.getPoolData(event.PoolAddress.Hex())
|
||||
if err != nil {
|
||||
@@ -176,6 +196,8 @@ func (s *MarketScanner) analyzeSwapEvent(event events.Event) {
|
||||
s.logger.Info(fmt.Sprintf("Found %d arbitrage opportunities for pool %s", len(opportunities), event.PoolAddress))
|
||||
for _, opp := range opportunities {
|
||||
s.logger.Info(fmt.Sprintf("Arbitrage opportunity: %+v", opp))
|
||||
// Execute the arbitrage opportunity
|
||||
s.executeArbitrageOpportunity(opp)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -191,6 +213,13 @@ func (s *MarketScanner) analyzeLiquidityEvent(event events.Event, isAdd bool) {
|
||||
}
|
||||
s.logger.Debug(fmt.Sprintf("Analyzing liquidity event (%s) in pool %s", action, event.PoolAddress))
|
||||
|
||||
// Log the liquidity event to database
|
||||
eventType := "add"
|
||||
if !isAdd {
|
||||
eventType = "remove"
|
||||
}
|
||||
s.logLiquidityEvent(event, eventType)
|
||||
|
||||
// Update cached pool data
|
||||
s.updatePoolData(event)
|
||||
|
||||
@@ -201,54 +230,27 @@ func (s *MarketScanner) analyzeLiquidityEvent(event events.Event, isAdd bool) {
|
||||
func (s *MarketScanner) analyzeNewPoolEvent(event events.Event) {
|
||||
s.logger.Info(fmt.Sprintf("New pool created: %s (protocol: %s)", event.PoolAddress, event.Protocol))
|
||||
|
||||
// Add to known pools
|
||||
// In a real implementation, you would want to fetch and cache the pool data
|
||||
s.logger.Debug(fmt.Sprintf("Added new pool %s to monitoring", event.PoolAddress))
|
||||
}
|
||||
// Add to known pools by fetching and caching the pool data
|
||||
s.logger.Debug(fmt.Sprintf("Adding new pool %s to monitoring", event.PoolAddress))
|
||||
|
||||
// calculatePriceMovement calculates the price movement from a swap event
|
||||
func (s *MarketScanner) calculatePriceMovement(event events.Event, poolData *CachedData) (*PriceMovement, error) {
|
||||
// Calculate the price before the swap using Uniswap V3 math
|
||||
priceBefore := uniswap.SqrtPriceX96ToPrice(poolData.SqrtPriceX96.ToBig())
|
||||
|
||||
// For a more accurate calculation, we would need to:
|
||||
// 1. Calculate the price after the swap using Uniswap V3 math
|
||||
// 2. Account for liquidity changes
|
||||
// 3. Consider the tick spacing and fee
|
||||
|
||||
priceMovement := &PriceMovement{
|
||||
Token0: event.Token0.Hex(),
|
||||
Token1: event.Token1.Hex(),
|
||||
Pool: event.PoolAddress.Hex(),
|
||||
Protocol: event.Protocol,
|
||||
AmountIn: new(big.Int).Set(event.Amount0),
|
||||
AmountOut: new(big.Int).Set(event.Amount1),
|
||||
PriceBefore: priceBefore,
|
||||
TickBefore: event.Tick,
|
||||
Timestamp: time.Now(), // In a real implementation, use the actual event timestamp
|
||||
// Fetch pool data to validate it's a real pool
|
||||
poolData, err := s.getPoolData(event.PoolAddress.Hex())
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to fetch data for new pool %s: %v", event.PoolAddress, err))
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate price impact using a more realistic approach
|
||||
// For Uniswap V3, price impact is roughly amountIn / liquidity
|
||||
if event.Liquidity != nil && event.Liquidity.Sign() > 0 && event.Amount0 != nil && event.Amount0.Sign() > 0 {
|
||||
liquidityFloat := new(big.Float).SetInt(event.Liquidity.ToBig())
|
||||
amountInFloat := new(big.Float).SetInt(event.Amount0)
|
||||
|
||||
// Price impact ≈ amountIn / liquidity
|
||||
priceImpact := new(big.Float).Quo(amountInFloat, liquidityFloat)
|
||||
priceImpactFloat, _ := priceImpact.Float64()
|
||||
priceMovement.PriceImpact = priceImpactFloat
|
||||
} else if priceMovement.AmountIn.Cmp(big.NewInt(0)) > 0 {
|
||||
// Fallback calculation
|
||||
impact := new(big.Float).Quo(
|
||||
new(big.Float).SetInt(priceMovement.AmountOut),
|
||||
new(big.Float).SetInt(priceMovement.AmountIn),
|
||||
)
|
||||
priceImpact, _ := impact.Float64()
|
||||
priceMovement.PriceImpact = priceImpact
|
||||
// Validate that this is a real pool contract
|
||||
if poolData.Address == (common.Address{}) {
|
||||
s.logger.Warn(fmt.Sprintf("Invalid pool contract at address %s", event.PoolAddress.Hex()))
|
||||
return
|
||||
}
|
||||
|
||||
return priceMovement, nil
|
||||
// Log pool data to database
|
||||
s.logPoolData(poolData)
|
||||
|
||||
s.logger.Info(fmt.Sprintf("Successfully added new pool %s to monitoring (tokens: %s-%s, fee: %d)",
|
||||
event.PoolAddress.Hex(), poolData.Token0.Hex(), poolData.Token1.Hex(), poolData.Fee))
|
||||
}
|
||||
|
||||
// isSignificantMovement determines if a price movement is significant enough to exploit
|
||||
@@ -277,17 +279,12 @@ func (s *MarketScanner) findRelatedPools(token0, token1 common.Address) []*Cache
|
||||
|
||||
relatedPools := make([]*CachedData, 0)
|
||||
|
||||
// In a real implementation, this would query a pool registry or
|
||||
// search through known pools for pools with the same token pair
|
||||
// For now, we'll return some mock data
|
||||
// Use dynamic pool discovery by checking known DEX factories
|
||||
poolAddresses := s.discoverPoolsForPair(token0, token1)
|
||||
|
||||
// Check if we have cached data for common pools
|
||||
commonPools := []string{
|
||||
"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", // USDC/WETH Uniswap V3 0.05%
|
||||
"0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc", // USDC/WETH Uniswap V2 0.3%
|
||||
}
|
||||
s.logger.Debug(fmt.Sprintf("Found %d potential pools for pair %s-%s", len(poolAddresses), token0.Hex(), token1.Hex()))
|
||||
|
||||
for _, poolAddr := range commonPools {
|
||||
for _, poolAddr := range poolAddresses {
|
||||
poolData, err := s.getPoolData(poolAddr)
|
||||
if err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("No data for pool %s: %v", poolAddr, err))
|
||||
@@ -305,16 +302,97 @@ func (s *MarketScanner) findRelatedPools(token0, token1 common.Address) []*Cache
|
||||
return relatedPools
|
||||
}
|
||||
|
||||
// estimateProfit estimates the potential profit from an arbitrage opportunity
|
||||
func (s *MarketScanner) estimateProfit(event events.Event, pool *CachedData, priceDiff float64) *big.Int {
|
||||
// This is a simplified profit estimation
|
||||
// In practice, this would involve complex calculations including:
|
||||
// - Precise Uniswap V3 math for swap calculations
|
||||
// - Gas cost estimation
|
||||
// - Slippage calculations
|
||||
// - Path optimization
|
||||
// discoverPoolsForPair discovers pools for a specific token pair using real factory contracts
|
||||
func (s *MarketScanner) discoverPoolsForPair(token0, token1 common.Address) []string {
|
||||
poolAddresses := make([]string, 0)
|
||||
|
||||
// For now, we'll use a simplified calculation
|
||||
// Use the CREATE2 calculator to find all possible pools
|
||||
pools, err := s.create2Calculator.FindPoolsForTokenPair(token0, token1)
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to discover pools for pair %s/%s: %v", token0.Hex(), token1.Hex(), err))
|
||||
return poolAddresses
|
||||
}
|
||||
|
||||
// Convert to string addresses
|
||||
for _, pool := range pools {
|
||||
poolAddresses = append(poolAddresses, pool.PoolAddr.Hex())
|
||||
}
|
||||
|
||||
s.logger.Debug(fmt.Sprintf("Discovered %d potential pools for pair %s/%s", len(poolAddresses), token0.Hex(), token1.Hex()))
|
||||
return poolAddresses
|
||||
}
|
||||
|
||||
// estimateProfit estimates the potential profit from an arbitrage opportunity using real slippage protection
|
||||
func (s *MarketScanner) estimateProfit(event events.Event, pool *CachedData, priceDiff float64) *big.Int {
|
||||
// Use comprehensive slippage analysis instead of simplified calculation
|
||||
if s.slippageProtector != nil {
|
||||
return s.calculateProfitWithSlippageProtection(event, pool, priceDiff)
|
||||
}
|
||||
|
||||
// Fallback to simplified calculation if slippage protection not available
|
||||
return s.calculateSimplifiedProfit(event, pool, priceDiff)
|
||||
}
|
||||
|
||||
// calculateProfitWithSlippageProtection uses slippage protection for accurate profit estimation
|
||||
func (s *MarketScanner) calculateProfitWithSlippageProtection(event events.Event, pool *CachedData, priceDiff float64) *big.Int {
|
||||
// Create trade parameters from event data
|
||||
tradeParams := &slippage.TradeParams{
|
||||
TokenIn: event.Token0,
|
||||
TokenOut: event.Token1,
|
||||
AmountIn: event.Amount0,
|
||||
PoolAddress: event.PoolAddress,
|
||||
Fee: big.NewInt(3000), // Assume 0.3% fee for now
|
||||
Deadline: uint64(time.Now().Add(5 * time.Minute).Unix()),
|
||||
}
|
||||
|
||||
// Analyze slippage with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
slippageResult, err := s.slippageProtector.AnalyzeSlippage(ctx, tradeParams)
|
||||
if err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("Slippage analysis failed: %v", err))
|
||||
return s.calculateSimplifiedProfit(event, pool, priceDiff)
|
||||
}
|
||||
|
||||
// Don't proceed if trade is not safe
|
||||
if !slippageResult.SafeToExecute || slippageResult.EmergencyStop {
|
||||
s.logger.Debug("Trade rejected by slippage protection")
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
// Calculate profit considering slippage
|
||||
expectedAmountOut := slippageResult.MaxAllowedAmountOut
|
||||
minAmountOut := slippageResult.MinRequiredAmountOut
|
||||
|
||||
// Profit = (expected_out - amount_in) - gas_costs - slippage_buffer
|
||||
profit := new(big.Int).Sub(expectedAmountOut, event.Amount0)
|
||||
|
||||
// REAL gas cost calculation for competitive MEV on Arbitrum
|
||||
// Base gas: 800k units, Price: 1.5 gwei, MEV premium: 15x = 0.018 ETH total
|
||||
baseGas := big.NewInt(800000) // 800k gas units for flash swap arbitrage
|
||||
gasPrice := big.NewInt(1500000000) // 1.5 gwei base price on Arbitrum
|
||||
mevPremium := big.NewInt(15) // 15x premium for MEV competition
|
||||
|
||||
gasCostWei := new(big.Int).Mul(baseGas, gasPrice)
|
||||
totalGasCost := new(big.Int).Mul(gasCostWei, mevPremium)
|
||||
|
||||
profit.Sub(profit, totalGasCost)
|
||||
|
||||
// Apply safety margin for slippage
|
||||
slippageMargin := new(big.Int).Sub(expectedAmountOut, minAmountOut)
|
||||
profit.Sub(profit, slippageMargin)
|
||||
|
||||
// Ensure profit is not negative
|
||||
if profit.Sign() < 0 {
|
||||
return big.NewInt(0)
|
||||
}
|
||||
|
||||
return profit
|
||||
}
|
||||
|
||||
// calculateSimplifiedProfit provides fallback profit calculation
|
||||
func (s *MarketScanner) calculateSimplifiedProfit(event events.Event, pool *CachedData, priceDiff float64) *big.Int {
|
||||
amountIn := new(big.Int).Set(event.Amount0)
|
||||
priceDiffInt := big.NewInt(int64(priceDiff * 1000000)) // Scale for integer math
|
||||
|
||||
@@ -322,9 +400,13 @@ func (s *MarketScanner) estimateProfit(event events.Event, pool *CachedData, pri
|
||||
profit := new(big.Int).Mul(amountIn, priceDiffInt)
|
||||
profit = profit.Div(profit, big.NewInt(1000000))
|
||||
|
||||
// Subtract estimated gas costs
|
||||
gasCost := big.NewInt(300000) // Rough estimate
|
||||
profit = profit.Sub(profit, gasCost)
|
||||
// REAL gas costs for multi-hop arbitrage
|
||||
baseGas := big.NewInt(1200000) // 1.2M gas for complex multi-hop
|
||||
gasPrice := big.NewInt(1500000000) // 1.5 gwei
|
||||
mevPremium := big.NewInt(20) // 20x premium for multi-hop MEV
|
||||
|
||||
totalGasCost := new(big.Int).Mul(new(big.Int).Mul(baseGas, gasPrice), mevPremium)
|
||||
profit = profit.Sub(profit, totalGasCost)
|
||||
|
||||
// Ensure profit is positive
|
||||
if profit.Sign() <= 0 {
|
||||
@@ -335,30 +417,168 @@ func (s *MarketScanner) estimateProfit(event events.Event, pool *CachedData, pri
|
||||
}
|
||||
|
||||
// findTriangularArbitrageOpportunities looks for triangular arbitrage opportunities
|
||||
func (s *MarketScanner) findTriangularArbitrageOpportunities(event events.Event) []ArbitrageOpportunity {
|
||||
s.logger.Debug(fmt.Sprintf("Searching for triangular arbitrage opportunities involving pool %s", event.PoolAddress.Hex()))
|
||||
func (s *MarketScanner) findTriangularArbitrageOpportunities(event events.Event) []types.ArbitrageOpportunity {
|
||||
s.logger.Debug(fmt.Sprintf("Searching for triangular arbitrage opportunities involving pool %s", event.PoolAddress))
|
||||
|
||||
opportunities := make([]ArbitrageOpportunity, 0)
|
||||
opportunities := make([]types.ArbitrageOpportunity, 0)
|
||||
|
||||
// This would implement logic to find triangular arbitrage paths like:
|
||||
// TokenA -> TokenB -> TokenC -> TokenA
|
||||
// where the end balance of TokenA is greater than the starting balance
|
||||
// Define common triangular paths on Arbitrum
|
||||
// Get triangular arbitrage paths from token configuration
|
||||
triangularPaths := tokens.GetTriangularPaths()
|
||||
|
||||
// For now, we'll return an empty slice
|
||||
// A full implementation would:
|
||||
// 1. Identify common triangular paths (e.g., USDC -> WETH -> WBTC -> USDC)
|
||||
// 2. Calculate the output of each leg of the trade
|
||||
// 3. Account for all fees and slippage
|
||||
// 4. Compare the final amount with the initial amount
|
||||
// Check if the event involves any tokens from our triangular paths
|
||||
eventInvolvesPaths := make([]int, 0)
|
||||
for i, path := range triangularPaths {
|
||||
for _, token := range path.Tokens {
|
||||
if token == event.Token0 || token == event.Token1 {
|
||||
eventInvolvesPaths = append(eventInvolvesPaths, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each relevant triangular path, calculate potential profit
|
||||
for _, pathIdx := range eventInvolvesPaths {
|
||||
path := triangularPaths[pathIdx]
|
||||
|
||||
// Define test amounts for arbitrage calculation
|
||||
testAmounts := []*big.Int{
|
||||
big.NewInt(1000000), // 1 USDC (6 decimals)
|
||||
big.NewInt(100000000), // 0.1 WETH (18 decimals)
|
||||
big.NewInt(10000000), // 0.01 WETH (18 decimals)
|
||||
}
|
||||
|
||||
for _, testAmount := range testAmounts {
|
||||
profit, gasEstimate, err := s.calculateTriangularProfit(path.Tokens, testAmount)
|
||||
if err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("Error calculating triangular profit for %s: %v", path.Name, err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if profitable after gas costs
|
||||
netProfit := new(big.Int).Sub(profit, gasEstimate)
|
||||
if netProfit.Sign() > 0 {
|
||||
// Calculate ROI
|
||||
roi := 0.0
|
||||
if testAmount.Sign() > 0 {
|
||||
roiFloat := new(big.Float).Quo(new(big.Float).SetInt(netProfit), new(big.Float).SetInt(testAmount))
|
||||
roi, _ = roiFloat.Float64()
|
||||
roi *= 100 // Convert to percentage
|
||||
}
|
||||
|
||||
// Create arbitrage opportunity
|
||||
tokenPaths := make([]string, len(path.Tokens))
|
||||
for i, token := range path.Tokens {
|
||||
tokenPaths[i] = token.Hex()
|
||||
}
|
||||
// Close the loop by adding the first token at the end
|
||||
tokenPaths = append(tokenPaths, path.Tokens[0].Hex())
|
||||
|
||||
opportunity := types.ArbitrageOpportunity{
|
||||
Path: tokenPaths,
|
||||
Pools: []string{}, // Pool addresses will be discovered dynamically
|
||||
Profit: netProfit,
|
||||
GasEstimate: gasEstimate,
|
||||
ROI: roi,
|
||||
Protocol: fmt.Sprintf("Triangular_%s", path.Name),
|
||||
}
|
||||
|
||||
opportunities = append(opportunities, opportunity)
|
||||
s.logger.Info(fmt.Sprintf("Found triangular arbitrage opportunity: %s, Profit: %s, ROI: %.2f%%",
|
||||
path.Name, netProfit.String(), roi))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return opportunities
|
||||
}
|
||||
|
||||
// calculateTriangularProfit calculates the profit from a triangular arbitrage path
|
||||
func (s *MarketScanner) calculateTriangularProfit(tokens []common.Address, initialAmount *big.Int) (*big.Int, *big.Int, error) {
|
||||
if len(tokens) < 3 {
|
||||
return nil, nil, fmt.Errorf("triangular arbitrage requires at least 3 tokens")
|
||||
}
|
||||
|
||||
currentAmount := new(big.Int).Set(initialAmount)
|
||||
totalGasCost := big.NewInt(0)
|
||||
|
||||
// Simulate trading through the triangular path
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
nextIndex := (i + 1) % len(tokens)
|
||||
tokenIn := tokens[i]
|
||||
tokenOut := tokens[nextIndex]
|
||||
|
||||
// Get pools that trade this token pair
|
||||
relatedPools := s.findRelatedPools(tokenIn, tokenOut)
|
||||
if len(relatedPools) == 0 {
|
||||
// No pools found for this pair, use estimation
|
||||
// Apply a 0.3% fee reduction as approximation
|
||||
currentAmount = new(big.Int).Mul(currentAmount, big.NewInt(997))
|
||||
currentAmount = new(big.Int).Div(currentAmount, big.NewInt(1000))
|
||||
} else {
|
||||
// Use the best pool for this trade
|
||||
bestPool := relatedPools[0]
|
||||
|
||||
// Calculate swap output using current amount
|
||||
outputAmount, err := s.calculateSwapOutput(currentAmount, bestPool, tokenIn, tokenOut)
|
||||
if err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("Error calculating swap output: %v", err))
|
||||
// Fallback to simple fee calculation
|
||||
currentAmount = new(big.Int).Mul(currentAmount, big.NewInt(997))
|
||||
currentAmount = new(big.Int).Div(currentAmount, big.NewInt(1000))
|
||||
} else {
|
||||
currentAmount = outputAmount
|
||||
}
|
||||
}
|
||||
|
||||
// Add gas cost for this hop (estimated)
|
||||
hopGas := big.NewInt(150000) // ~150k gas per swap
|
||||
totalGasCost.Add(totalGasCost, hopGas)
|
||||
}
|
||||
|
||||
// Calculate profit (final amount - initial amount)
|
||||
profit := new(big.Int).Sub(currentAmount, initialAmount)
|
||||
|
||||
return profit, totalGasCost, nil
|
||||
}
|
||||
|
||||
// calculateSwapOutput calculates the output amount for a token swap
|
||||
func (s *MarketScanner) calculateSwapOutput(amountIn *big.Int, pool *CachedData, tokenIn, tokenOut common.Address) (*big.Int, error) {
|
||||
if pool.SqrtPriceX96 == nil || pool.Liquidity == nil {
|
||||
return nil, fmt.Errorf("missing pool price or liquidity data")
|
||||
}
|
||||
|
||||
// Convert sqrtPriceX96 to price for calculation
|
||||
price := uniswap.SqrtPriceX96ToPrice(pool.SqrtPriceX96.ToBig())
|
||||
|
||||
// Simple approximation: apply price and fee
|
||||
amountInFloat := new(big.Float).SetInt(amountIn)
|
||||
var amountOut *big.Float
|
||||
|
||||
if tokenIn == pool.Token0 {
|
||||
// Token0 -> Token1: multiply by price
|
||||
amountOut = new(big.Float).Mul(amountInFloat, price)
|
||||
} else {
|
||||
// Token1 -> Token0: divide by price
|
||||
amountOut = new(big.Float).Quo(amountInFloat, price)
|
||||
}
|
||||
|
||||
// Apply fee (assume 0.3% for simplicity)
|
||||
feeRate := big.NewFloat(0.997) // 1 - 0.003
|
||||
amountOut.Mul(amountOut, feeRate)
|
||||
|
||||
// Convert back to big.Int
|
||||
result := new(big.Int)
|
||||
amountOut.Int(result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// findArbitrageOpportunities looks for arbitrage opportunities based on price movements
|
||||
func (s *MarketScanner) findArbitrageOpportunities(event events.Event, movement *PriceMovement) []ArbitrageOpportunity {
|
||||
func (s *MarketScanner) findArbitrageOpportunities(event events.Event, movement *PriceMovement) []types.ArbitrageOpportunity {
|
||||
s.logger.Debug(fmt.Sprintf("Searching for arbitrage opportunities for pool %s", event.PoolAddress))
|
||||
|
||||
opportunities := make([]ArbitrageOpportunity, 0)
|
||||
opportunities := make([]types.ArbitrageOpportunity, 0)
|
||||
|
||||
// Get related pools for the same token pair
|
||||
relatedPools := s.findRelatedPools(event.Token0, event.Token1)
|
||||
@@ -408,7 +628,7 @@ func (s *MarketScanner) findArbitrageOpportunities(event events.Event, movement
|
||||
estimatedProfit := s.estimateProfit(event, pool, priceDiffFloat)
|
||||
|
||||
if estimatedProfit != nil && estimatedProfit.Sign() > 0 {
|
||||
opp := ArbitrageOpportunity{
|
||||
opp := types.ArbitrageOpportunity{
|
||||
Path: []string{event.Token0.Hex(), event.Token1.Hex()},
|
||||
Pools: []string{event.PoolAddress.Hex(), pool.Address.Hex()},
|
||||
Profit: estimatedProfit,
|
||||
@@ -430,25 +650,135 @@ func (s *MarketScanner) findArbitrageOpportunities(event events.Event, movement
|
||||
return opportunities
|
||||
}
|
||||
|
||||
// Stop stops the market scanner and all workers
|
||||
func (s *MarketScanner) Stop() {
|
||||
// Stop all workers
|
||||
for _, worker := range s.workers {
|
||||
worker.Stop()
|
||||
// executeArbitrageOpportunity executes an arbitrage opportunity using the smart contract
|
||||
func (s *MarketScanner) executeArbitrageOpportunity(opportunity types.ArbitrageOpportunity) {
|
||||
// Check if contract executor is available
|
||||
if s.contractExecutor == nil {
|
||||
s.logger.Warn("Contract executor not available, skipping arbitrage execution")
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for all jobs to complete
|
||||
s.wg.Wait()
|
||||
// Only execute opportunities with sufficient profit
|
||||
minProfitThreshold := big.NewInt(10000000000000000) // 0.01 ETH minimum profit
|
||||
if opportunity.Profit.Cmp(minProfitThreshold) < 0 {
|
||||
s.logger.Debug(fmt.Sprintf("Arbitrage opportunity profit too low: %s < %s",
|
||||
opportunity.Profit.String(), minProfitThreshold.String()))
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info(fmt.Sprintf("Executing arbitrage opportunity with profit: %s", opportunity.Profit.String()))
|
||||
|
||||
// Execute the arbitrage opportunity
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var tx *types.Transaction
|
||||
var err error
|
||||
|
||||
// Determine if this is a triangular arbitrage or standard arbitrage
|
||||
if len(opportunity.Path) == 3 && len(opportunity.Pools) == 3 {
|
||||
// Triangular arbitrage
|
||||
tx, err = s.contractExecutor.ExecuteTriangularArbitrage(ctx, opportunity)
|
||||
} else {
|
||||
// Standard arbitrage
|
||||
tx, err = s.contractExecutor.ExecuteArbitrage(ctx, opportunity)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error(fmt.Sprintf("Failed to execute arbitrage opportunity: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info(fmt.Sprintf("Arbitrage transaction submitted: %s", tx.Hash().Hex()))
|
||||
}
|
||||
|
||||
// ArbitrageOpportunity represents a potential arbitrage opportunity
|
||||
type ArbitrageOpportunity struct {
|
||||
Path []string // Token path for the arbitrage
|
||||
Pools []string // Pools involved in the arbitrage
|
||||
Profit *big.Int // Estimated profit in wei
|
||||
GasEstimate *big.Int // Estimated gas cost
|
||||
ROI float64 // Return on investment percentage
|
||||
Protocol string // DEX protocol
|
||||
// logSwapEvent logs a swap event to the database
|
||||
func (s *MarketScanner) logSwapEvent(event events.Event) {
|
||||
if s.database == nil {
|
||||
return // Database not available
|
||||
}
|
||||
|
||||
// Convert event to database record
|
||||
swapEvent := &database.SwapEvent{
|
||||
Timestamp: time.Now(),
|
||||
BlockNumber: event.BlockNumber,
|
||||
TxHash: event.TxHash,
|
||||
PoolAddress: event.PoolAddress,
|
||||
Token0: event.Token0,
|
||||
Token1: event.Token1,
|
||||
Amount0In: event.Amount0,
|
||||
Amount1In: event.Amount1,
|
||||
Amount0Out: big.NewInt(0), // Would need to calculate from event data
|
||||
Amount1Out: big.NewInt(0), // Would need to calculate from event data
|
||||
Sender: common.Address{}, // Would need to extract from transaction
|
||||
Recipient: common.Address{}, // Would need to extract from transaction
|
||||
Protocol: event.Protocol,
|
||||
}
|
||||
|
||||
// Log the swap event asynchronously to avoid blocking
|
||||
go func() {
|
||||
if err := s.database.InsertSwapEvent(swapEvent); err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("Failed to log swap event: %v", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// logLiquidityEvent logs a liquidity event to the database
|
||||
func (s *MarketScanner) logLiquidityEvent(event events.Event, eventType string) {
|
||||
if s.database == nil {
|
||||
return // Database not available
|
||||
}
|
||||
|
||||
// Convert event to database record
|
||||
liquidityEvent := &database.LiquidityEvent{
|
||||
Timestamp: time.Now(),
|
||||
BlockNumber: event.BlockNumber,
|
||||
TxHash: event.TxHash,
|
||||
PoolAddress: event.PoolAddress,
|
||||
Token0: event.Token0,
|
||||
Token1: event.Token1,
|
||||
Liquidity: event.Liquidity,
|
||||
Amount0: event.Amount0,
|
||||
Amount1: event.Amount1,
|
||||
Sender: common.Address{}, // Would need to extract from transaction
|
||||
Recipient: common.Address{}, // Would need to extract from transaction
|
||||
EventType: eventType,
|
||||
Protocol: event.Protocol,
|
||||
}
|
||||
|
||||
// Log the liquidity event asynchronously to avoid blocking
|
||||
go func() {
|
||||
if err := s.database.InsertLiquidityEvent(liquidityEvent); err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("Failed to log liquidity event: %v", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// logPoolData logs pool data to the database
|
||||
func (s *MarketScanner) logPoolData(poolData *CachedData) {
|
||||
if s.database == nil {
|
||||
return // Database not available
|
||||
}
|
||||
|
||||
// Convert cached data to database record
|
||||
dbPoolData := &database.PoolData{
|
||||
Address: poolData.Address,
|
||||
Token0: poolData.Token0,
|
||||
Token1: poolData.Token1,
|
||||
Fee: poolData.Fee,
|
||||
Liquidity: poolData.Liquidity.ToBig(),
|
||||
SqrtPriceX96: poolData.SqrtPriceX96.ToBig(),
|
||||
Tick: int64(poolData.Tick),
|
||||
LastUpdated: time.Now(),
|
||||
Protocol: poolData.Protocol,
|
||||
}
|
||||
|
||||
// Log the pool data asynchronously to avoid blocking
|
||||
go func() {
|
||||
if err := s.database.InsertPoolData(dbPoolData); err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("Failed to log pool data: %v", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// PriceMovement represents a potential price movement
|
||||
@@ -518,48 +848,129 @@ func (s *MarketScanner) getPoolData(poolAddress string) (*CachedData, error) {
|
||||
func (s *MarketScanner) fetchPoolData(poolAddress string) (*CachedData, error) {
|
||||
s.logger.Debug(fmt.Sprintf("Fetching pool data for %s", poolAddress))
|
||||
|
||||
// This is a simplified implementation
|
||||
// In practice, you would interact with the Ethereum blockchain to get real data
|
||||
address := common.HexToAddress(poolAddress)
|
||||
|
||||
// For now, we'll return mock data
|
||||
pool := &CachedData{
|
||||
// In test environment, return mock data to avoid network calls
|
||||
if s.isTestEnvironment() {
|
||||
return s.getMockPoolData(poolAddress), nil
|
||||
}
|
||||
|
||||
// Create RPC client connection
|
||||
// Get RPC endpoint from config or environment
|
||||
rpcEndpoint := os.Getenv("ARBITRUM_RPC_ENDPOINT")
|
||||
if rpcEndpoint == "" {
|
||||
rpcEndpoint = "wss://arbitrum-mainnet.core.chainstack.com/f69d14406bc00700da9b936504e1a870" // fallback
|
||||
}
|
||||
client, err := ethclient.Dial(rpcEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Ethereum node: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Create Uniswap V3 pool interface
|
||||
pool := uniswap.NewUniswapV3Pool(address, client)
|
||||
|
||||
// Validate that this is a real pool contract
|
||||
if !uniswap.IsValidPool(context.Background(), client, address) {
|
||||
return nil, fmt.Errorf("invalid pool contract at address %s", address.Hex())
|
||||
}
|
||||
|
||||
// Fetch real pool state from the blockchain
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
poolState, err := pool.GetPoolState(ctx)
|
||||
if err != nil {
|
||||
s.logger.Warn(fmt.Sprintf("Failed to fetch real pool state for %s: %v", address.Hex(), err))
|
||||
return nil, fmt.Errorf("failed to fetch pool state: %w", err)
|
||||
}
|
||||
|
||||
// Determine tick spacing based on fee tier
|
||||
tickSpacing := 60 // Default for 0.3% fee
|
||||
switch poolState.Fee {
|
||||
case 100: // 0.01%
|
||||
tickSpacing = 1
|
||||
case 500: // 0.05%
|
||||
tickSpacing = 10
|
||||
case 3000: // 0.3%
|
||||
tickSpacing = 60
|
||||
case 10000: // 1%
|
||||
tickSpacing = 200
|
||||
}
|
||||
|
||||
// Determine protocol (assume UniswapV3 for now, could be enhanced to detect protocol)
|
||||
protocol := "UniswapV3"
|
||||
|
||||
// Create pool data from real blockchain state
|
||||
poolData := &CachedData{
|
||||
Address: address,
|
||||
Token0: common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), // USDC
|
||||
Token1: common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), // WETH
|
||||
Fee: 3000, // 0.3%
|
||||
Liquidity: uint256.NewInt(1000000000000000000), // 1 ETH equivalent
|
||||
SqrtPriceX96: uint256.NewInt(2505414483750470000), // Mock sqrt price
|
||||
Tick: 200000, // Mock tick
|
||||
TickSpacing: 60, // Tick spacing for 0.3% fee
|
||||
Protocol: "UniswapV3", // Mock protocol
|
||||
Token0: poolState.Token0,
|
||||
Token1: poolState.Token1,
|
||||
Fee: poolState.Fee,
|
||||
Liquidity: poolState.Liquidity,
|
||||
SqrtPriceX96: poolState.SqrtPriceX96,
|
||||
Tick: poolState.Tick,
|
||||
TickSpacing: tickSpacing,
|
||||
Protocol: protocol,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
s.logger.Debug(fmt.Sprintf("Fetched pool data for %s", poolAddress))
|
||||
return pool, nil
|
||||
s.logger.Info(fmt.Sprintf("Fetched real pool data for %s: Token0=%s, Token1=%s, Fee=%d, Liquidity=%s",
|
||||
address.Hex(), poolState.Token0.Hex(), poolState.Token1.Hex(), poolState.Fee, poolState.Liquidity.String()))
|
||||
|
||||
return poolData, nil
|
||||
}
|
||||
|
||||
// updatePoolData updates cached pool data
|
||||
// updatePoolData updates cached pool data from an event
|
||||
func (s *MarketScanner) updatePoolData(event events.Event) {
|
||||
cacheKey := fmt.Sprintf("pool_%s", event.PoolAddress.Hex())
|
||||
poolKey := event.PoolAddress.Hex()
|
||||
|
||||
s.cacheMutex.Lock()
|
||||
defer s.cacheMutex.Unlock()
|
||||
|
||||
// Update existing cache entry or create new one
|
||||
data := &CachedData{
|
||||
Address: event.PoolAddress,
|
||||
Token0: event.Token0,
|
||||
Token1: event.Token1,
|
||||
Liquidity: event.Liquidity,
|
||||
SqrtPriceX96: event.SqrtPriceX96,
|
||||
Tick: event.Tick,
|
||||
Protocol: event.Protocol, // Add protocol information
|
||||
LastUpdated: time.Now(),
|
||||
if pool, exists := s.cache[poolKey]; exists {
|
||||
// Update liquidity if provided
|
||||
if event.Liquidity != nil {
|
||||
pool.Liquidity = event.Liquidity
|
||||
}
|
||||
|
||||
// Update sqrtPriceX96 if provided
|
||||
if event.SqrtPriceX96 != nil {
|
||||
pool.SqrtPriceX96 = event.SqrtPriceX96
|
||||
}
|
||||
|
||||
// Update tick if provided
|
||||
if event.Tick != 0 {
|
||||
pool.Tick = event.Tick
|
||||
}
|
||||
|
||||
// Update last updated time
|
||||
pool.LastUpdated = time.Now()
|
||||
|
||||
// Log updated pool data to database
|
||||
s.logPoolData(pool)
|
||||
} else {
|
||||
// Create new pool entry
|
||||
pool := &CachedData{
|
||||
Address: event.PoolAddress,
|
||||
Token0: event.Token0,
|
||||
Token1: event.Token1,
|
||||
Fee: event.Fee,
|
||||
Liquidity: event.Liquidity,
|
||||
SqrtPriceX96: event.SqrtPriceX96,
|
||||
Tick: event.Tick,
|
||||
TickSpacing: getTickSpacing(event.Fee),
|
||||
Protocol: event.Protocol,
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
|
||||
s.cache[poolKey] = pool
|
||||
|
||||
// Log new pool data to database
|
||||
s.logPoolData(pool)
|
||||
}
|
||||
|
||||
s.cache[cacheKey] = data
|
||||
s.logger.Debug(fmt.Sprintf("Updated cache for pool %s", event.PoolAddress.Hex()))
|
||||
}
|
||||
|
||||
@@ -582,3 +993,71 @@ func (s *MarketScanner) cleanupCache() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isTestEnvironment checks if we're running in a test environment
|
||||
func (s *MarketScanner) isTestEnvironment() bool {
|
||||
// Check for explicit test environment variable
|
||||
if os.Getenv("GO_TEST") == "true" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for testing framework flags
|
||||
for _, arg := range os.Args {
|
||||
if strings.HasPrefix(arg, "-test.") || arg == "test" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the program name is from 'go test'
|
||||
progName := os.Args[0]
|
||||
if strings.Contains(progName, ".test") || strings.HasSuffix(progName, ".test") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if running under go test command
|
||||
if strings.Contains(progName, "go_build_") && strings.Contains(progName, "_test") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Default to production mode - NEVER return true by default
|
||||
return false
|
||||
}
|
||||
|
||||
// getMockPoolData returns mock pool data for testing
|
||||
func (s *MarketScanner) getMockPoolData(poolAddress string) *CachedData {
|
||||
// Create deterministic mock data based on pool address
|
||||
mockTokens := tokens.GetArbitrumTokens()
|
||||
|
||||
// Use different token pairs based on pool address
|
||||
var token0, token1 common.Address
|
||||
switch poolAddress {
|
||||
case "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640":
|
||||
token0 = mockTokens.USDC
|
||||
token1 = mockTokens.WETH
|
||||
case "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc":
|
||||
token0 = mockTokens.USDC
|
||||
token1 = mockTokens.WETH
|
||||
default:
|
||||
token0 = mockTokens.USDC
|
||||
token1 = mockTokens.WETH
|
||||
}
|
||||
|
||||
// Convert big.Int to uint256.Int for compatibility
|
||||
liquidity := uint256.NewInt(1000000000000000000) // 1 ETH equivalent
|
||||
|
||||
// Create a reasonable sqrtPriceX96 value for ~2000 USDC per ETH
|
||||
sqrtPrice, _ := uint256.FromHex("0x668F0BD9C5DB9D2F2DF6A0E4C") // Reasonable value
|
||||
|
||||
return &CachedData{
|
||||
Address: common.HexToAddress(poolAddress),
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
Fee: 3000, // 0.3%
|
||||
TickSpacing: 60,
|
||||
Liquidity: liquidity,
|
||||
SqrtPriceX96: sqrtPrice,
|
||||
Tick: -74959, // Corresponds to the sqrt price above
|
||||
Protocol: "UniswapV3",
|
||||
LastUpdated: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user