Files
mev-beta/pkg/arbitrage/flash_executor.go
2025-10-04 09:31:02 -05:00

817 lines
27 KiB
Go

package arbitrage
import (
"context"
"fmt"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/arbitrum"
"github.com/fraktal/mev-beta/pkg/math"
"github.com/fraktal/mev-beta/pkg/security"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
)
// FlashSwapExecutor executes arbitrage using flash swaps for capital efficiency
type FlashSwapExecutor struct {
client *ethclient.Client
logger *logger.Logger
keyManager *security.KeyManager
gasEstimator *arbitrum.L2GasEstimator
decimalConverter *math.DecimalConverter
// Contract addresses
flashSwapContract common.Address
arbitrageContract common.Address
// Configuration
config ExecutionConfig
// State tracking
pendingExecutions map[common.Hash]*ExecutionState
executionHistory []*ExecutionResult
totalProfit *math.UniversalDecimal
totalGasCost *math.UniversalDecimal
}
// ExecutionConfig configures the flash swap executor
type ExecutionConfig struct {
// Risk management
MaxSlippage *math.UniversalDecimal
MinProfitThreshold *math.UniversalDecimal
MaxPositionSize *math.UniversalDecimal
MaxDailyVolume *math.UniversalDecimal
// Gas settings
GasLimitMultiplier float64
MaxGasPrice *math.UniversalDecimal
PriorityFeeStrategy string // "conservative", "aggressive", "competitive"
// Execution settings
ExecutionTimeout time.Duration
ConfirmationBlocks uint64
RetryAttempts int
RetryDelay time.Duration
// MEV protection
EnableMEVProtection bool
PrivateMempool bool
FlashbotsRelay string
// Monitoring
EnableDetailedLogs bool
TrackPerformance bool
}
// ExecutionState tracks the state of an ongoing execution
type ExecutionState struct {
Opportunity *pkgtypes.ArbitrageOpportunity
TransactionHash common.Hash
Status ExecutionStatus
StartTime time.Time
SubmissionTime time.Time
ConfirmationTime time.Time
GasUsed uint64
EffectiveGasPrice *big.Int
ActualProfit *math.UniversalDecimal
Error error
}
// ExecutionStatus represents the current status of an execution
type ExecutionStatus string
const (
StatusPending ExecutionStatus = "pending"
StatusSubmitted ExecutionStatus = "submitted"
StatusConfirmed ExecutionStatus = "confirmed"
StatusFailed ExecutionStatus = "failed"
StatusReverted ExecutionStatus = "reverted"
)
// FlashSwapCalldata represents the data needed for a flash swap execution
type FlashSwapCalldata struct {
InitiatorPool common.Address
TokenPath []common.Address
Pools []common.Address
AmountIn *big.Int
MinAmountOut *big.Int
Recipient common.Address
Data []byte
}
// NewFlashSwapExecutor creates a new flash swap executor
func NewFlashSwapExecutor(
client *ethclient.Client,
logger *logger.Logger,
keyManager *security.KeyManager,
gasEstimator *arbitrum.L2GasEstimator,
flashSwapContract,
arbitrageContract common.Address,
config ExecutionConfig,
) *FlashSwapExecutor {
executor := &FlashSwapExecutor{
client: client,
logger: logger,
keyManager: keyManager,
gasEstimator: gasEstimator,
decimalConverter: math.NewDecimalConverter(),
flashSwapContract: flashSwapContract,
arbitrageContract: arbitrageContract,
config: config,
pendingExecutions: make(map[common.Hash]*ExecutionState),
executionHistory: make([]*ExecutionResult, 0),
}
// Initialize counters
executor.totalProfit, _ = executor.decimalConverter.FromString("0", 18, "ETH")
executor.totalGasCost, _ = executor.decimalConverter.FromString("0", 18, "ETH")
// Set default configuration
executor.setDefaultConfig()
return executor
}
// setDefaultConfig sets default configuration values
func (executor *FlashSwapExecutor) setDefaultConfig() {
if executor.config.MaxSlippage == nil {
executor.config.MaxSlippage, _ = executor.decimalConverter.FromString("1", 4, "PERCENT") // 1%
}
if executor.config.MinProfitThreshold == nil {
executor.config.MinProfitThreshold, _ = executor.decimalConverter.FromString("0.01", 18, "ETH")
}
if executor.config.MaxPositionSize == nil {
executor.config.MaxPositionSize, _ = executor.decimalConverter.FromString("10", 18, "ETH")
}
if executor.config.GasLimitMultiplier == 0 {
executor.config.GasLimitMultiplier = 1.2 // 20% buffer
}
if executor.config.ExecutionTimeout == 0 {
executor.config.ExecutionTimeout = 30 * time.Second
}
if executor.config.ConfirmationBlocks == 0 {
executor.config.ConfirmationBlocks = 1 // Arbitrum has fast finality
}
if executor.config.RetryAttempts == 0 {
executor.config.RetryAttempts = 3
}
if executor.config.RetryDelay == 0 {
executor.config.RetryDelay = 2 * time.Second
}
if executor.config.PriorityFeeStrategy == "" {
executor.config.PriorityFeeStrategy = "competitive"
}
}
// ExecuteArbitrage executes an arbitrage opportunity using flash swaps
func (executor *FlashSwapExecutor) ExecuteArbitrage(ctx context.Context, opportunity *pkgtypes.ArbitrageOpportunity) (*ExecutionResult, error) {
executor.logger.Info(fmt.Sprintf("🚀 Executing arbitrage opportunity: %s profit expected",
opportunity.NetProfit.String()))
// Validate opportunity before execution
if err := executor.validateOpportunity(opportunity); err != nil {
return nil, fmt.Errorf("opportunity validation failed: %w", err)
}
// Create execution state
executionState := &ExecutionState{
Opportunity: opportunity,
Status: StatusPending,
StartTime: time.Now(),
}
// Prepare flash swap transaction
flashSwapData, err := executor.prepareFlashSwap(opportunity)
if err != nil {
result := executor.createFailedResult(executionState, fmt.Errorf("failed to prepare flash swap: %w", err))
return result, nil
}
// Get transaction options with dynamic gas pricing
transactOpts, err := executor.getTransactionOptions(ctx, flashSwapData)
if err != nil {
return executor.createFailedResult(executionState, fmt.Errorf("failed to get transaction options: %w", err)), nil
}
// Execute with retry logic
var result *ExecutionResult
for attempt := 0; attempt <= executor.config.RetryAttempts; attempt++ {
if attempt > 0 {
executor.logger.Info(fmt.Sprintf("Retrying execution attempt %d/%d", attempt, executor.config.RetryAttempts))
time.Sleep(executor.config.RetryDelay)
}
result = executor.executeWithTimeout(ctx, executionState, flashSwapData, transactOpts)
// If successful or non-retryable error, break
errorMsg := ""
if result.Error != nil {
errorMsg = result.Error.Error()
}
if result.Success || !executor.isRetryableError(errorMsg) {
break
}
// Update gas price for retry
if attempt < executor.config.RetryAttempts {
transactOpts, err = executor.updateGasPriceForRetry(ctx, transactOpts, attempt)
if err != nil {
executor.logger.Warn(fmt.Sprintf("Failed to update gas price for retry: %v", err))
}
}
}
// Update statistics
executor.updateExecutionStats(result)
status := "Unknown"
if result.Success {
status = "Success"
} else if result.Error != nil {
status = "Failed"
} else {
status = "Incomplete"
}
executor.logger.Info(fmt.Sprintf("✅ Arbitrage execution completed: %s", status))
if result.Success && result.ProfitRealized != nil {
// Note: opportunity.NetProfit is not directly accessible through ExecutionResult structure
// So we just log that execution was successful with actual profit
executor.logger.Info(fmt.Sprintf("💰 Actual profit: %s ETH",
formatEther(result.ProfitRealized)))
}
return result, nil
}
// validateOpportunity validates an opportunity before execution
func (executor *FlashSwapExecutor) validateOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) error {
// Check minimum profit threshold
minProfitWei := big.NewInt(10000000000000000) // 0.01 ETH in wei
if opportunity.NetProfit.Cmp(minProfitWei) < 0 {
return fmt.Errorf("profit %s below minimum threshold %s",
opportunity.NetProfit.String(),
minProfitWei.String())
}
// Check maximum position size
maxPositionWei := big.NewInt(1000000000000000000) // 1 ETH in wei
if opportunity.AmountIn.Cmp(maxPositionWei) > 0 {
return fmt.Errorf("position size %s exceeds maximum %s",
opportunity.AmountIn.String(),
maxPositionWei.String())
}
// Check price impact
maxPriceImpact := 5.0 // 5% max
if opportunity.PriceImpact > maxPriceImpact {
return fmt.Errorf("price impact %.2f%% too high",
opportunity.PriceImpact)
}
// Check confidence level
if opportunity.Confidence < 0.7 {
return fmt.Errorf("confidence level %.1f%% too low", opportunity.Confidence*100)
}
// Check execution path
if len(opportunity.Path) < 2 {
return fmt.Errorf("empty execution path")
}
// Basic validation for path
if len(opportunity.Path) < 2 {
return fmt.Errorf("path must have at least 2 tokens")
}
return nil
}
// prepareFlashSwap prepares the flash swap transaction data
func (executor *FlashSwapExecutor) prepareFlashSwap(opportunity *pkgtypes.ArbitrageOpportunity) (*FlashSwapCalldata, error) {
if len(opportunity.Path) < 2 {
return nil, fmt.Errorf("path must have at least 2 tokens")
}
// Convert path strings to token addresses
tokenPath := make([]common.Address, 0, len(opportunity.Path))
for _, tokenAddr := range opportunity.Path {
tokenPath = append(tokenPath, common.HexToAddress(tokenAddr))
}
// Use pool addresses from opportunity if available
poolAddresses := make([]common.Address, 0, len(opportunity.Pools))
for _, poolAddr := range opportunity.Pools {
poolAddresses = append(poolAddresses, common.HexToAddress(poolAddr))
}
// Calculate minimum output with slippage protection
expectedOutput := opportunity.Profit
// Calculate minimum output with slippage protection using basic math
slippagePercent := opportunity.MaxSlippage / 100.0 // Convert percentage to decimal
slippageFactor := big.NewFloat(1.0 - slippagePercent)
expectedFloat := new(big.Float).SetInt(expectedOutput)
minOutputFloat := new(big.Float).Mul(expectedFloat, slippageFactor)
minAmountOut, _ := minOutputFloat.Int(nil)
// Ensure minAmountOut is not negative
if minAmountOut.Sign() < 0 {
minAmountOut = big.NewInt(0)
}
// Create flash swap data
calldata := &FlashSwapCalldata{
InitiatorPool: poolAddresses[0], // First pool initiates the flash swap
TokenPath: tokenPath,
Pools: poolAddresses,
AmountIn: opportunity.AmountIn,
MinAmountOut: minAmountOut,
Recipient: executor.arbitrageContract, // Our arbitrage contract
Data: executor.encodeArbitrageData(opportunity),
}
return calldata, nil
}
// encodeArbitrageData encodes the arbitrage execution data
func (executor *FlashSwapExecutor) encodeArbitrageData(opportunity *pkgtypes.ArbitrageOpportunity) []byte {
// In production, this would properly ABI-encode the arbitrage parameters
// For demonstration, we'll create a simple encoding that includes key parameters
// This is a simplified approach - real implementation would use proper ABI encoding
// with go-ethereum's abi package
data := []byte(fmt.Sprintf("arbitrage:%s:%s:%s:%s",
opportunity.TokenIn,
opportunity.TokenOut,
opportunity.AmountIn.String(),
opportunity.Profit.String()))
if len(data) > 1024 { // Limit the size
data = data[:1024]
}
return data
}
// getTransactionOptions prepares transaction options with dynamic gas pricing
func (executor *FlashSwapExecutor) getTransactionOptions(ctx context.Context, flashSwapData *FlashSwapCalldata) (*bind.TransactOpts, error) {
// Get active private key
privateKey, err := executor.keyManager.GetActivePrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to get private key: %w", err)
}
// Get chain ID
chainID, err := executor.client.ChainID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}
// Create transaction options
transactOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
if err != nil {
return nil, fmt.Errorf("failed to create transactor: %w", err)
}
// For gas estimation, we would normally call the contract method with callMsg
// Since we're using a mock implementation, we'll use a reasonable default
// In production, you'd do: gasLimit, err := client.EstimateGas(ctx, callMsg)
// For demonstration purposes, we'll use a reasonable default gas limit
estimatedGas := uint64(800000) // Standard for complex flash swaps
// Apply gas limit multiplier
adjustedGasLimit := uint64(float64(estimatedGas) * executor.config.GasLimitMultiplier)
transactOpts.GasLimit = adjustedGasLimit
// Get gas price from network for proper EIP-1559 transaction
suggestedTip, err := executor.client.SuggestGasTipCap(ctx)
if err != nil {
// Default priority fee
suggestedTip = big.NewInt(100000000) // 0.1 gwei
}
baseFee, err := executor.client.HeaderByNumber(ctx, nil)
if err != nil || baseFee.BaseFee == nil {
// For networks that don't support EIP-1559 or on error
defaultBaseFee := big.NewInt(1000000000) // 1 gwei
transactOpts.GasFeeCap = new(big.Int).Add(defaultBaseFee, suggestedTip)
} else {
// EIP-1559 gas pricing: FeeCap = baseFee*2 + priorityFee
transactOpts.GasFeeCap = new(big.Int).Add(
new(big.Int).Mul(baseFee.BaseFee, big.NewInt(2)),
suggestedTip,
)
}
transactOpts.GasTipCap = suggestedTip
executor.logger.Debug(fmt.Sprintf("Gas estimate - Limit: %d, MaxFee: %s, Priority: %s",
adjustedGasLimit,
transactOpts.GasFeeCap.String(),
transactOpts.GasTipCap.String()))
// Apply priority fee strategy
executor.applyPriorityFeeStrategy(transactOpts)
return transactOpts, nil
}
// applyPriorityFeeStrategy adjusts gas pricing based on strategy
func (executor *FlashSwapExecutor) applyPriorityFeeStrategy(transactOpts *bind.TransactOpts) {
switch executor.config.PriorityFeeStrategy {
case "aggressive":
// Increase priority fee by 50%
if transactOpts.GasTipCap != nil {
newTip := new(big.Int).Mul(transactOpts.GasTipCap, big.NewInt(150))
transactOpts.GasTipCap = new(big.Int).Div(newTip, big.NewInt(100))
}
case "competitive":
// Increase priority fee by 25%
if transactOpts.GasTipCap != nil {
newTip := new(big.Int).Mul(transactOpts.GasTipCap, big.NewInt(125))
transactOpts.GasTipCap = new(big.Int).Div(newTip, big.NewInt(100))
}
case "conservative":
// Use default priority fee (no change)
}
// Ensure we don't exceed maximum gas price
if executor.config.MaxGasPrice != nil && transactOpts.GasFeeCap != nil {
if transactOpts.GasFeeCap.Cmp(big.NewInt(50000000000)) > 0 {
transactOpts.GasFeeCap = new(big.Int).Set(big.NewInt(50000000000))
}
}
}
// executeWithTimeout executes the flash swap with timeout protection
func (executor *FlashSwapExecutor) executeWithTimeout(
ctx context.Context,
executionState *ExecutionState,
flashSwapData *FlashSwapCalldata,
transactOpts *bind.TransactOpts,
) *ExecutionResult {
// Create timeout context
timeoutCtx, cancel := context.WithTimeout(ctx, executor.config.ExecutionTimeout)
defer cancel()
// Submit transaction
tx, err := executor.submitTransaction(timeoutCtx, flashSwapData, transactOpts)
if err != nil {
return executor.createFailedResult(executionState, fmt.Errorf("transaction submission failed: %w", err))
}
executionState.TransactionHash = tx.Hash()
executionState.Status = StatusSubmitted
executionState.SubmissionTime = time.Now()
executor.pendingExecutions[tx.Hash()] = executionState
executor.logger.Info(fmt.Sprintf("📤 Transaction submitted: %s", tx.Hash().Hex()))
// Wait for confirmation
receipt, err := executor.waitForConfirmation(timeoutCtx, tx.Hash())
if err != nil {
return executor.createFailedResult(executionState, fmt.Errorf("confirmation failed: %w", err))
}
executionState.ConfirmationTime = time.Now()
executionState.GasUsed = receipt.GasUsed
executionState.EffectiveGasPrice = receipt.EffectiveGasPrice
// Check transaction status
if receipt.Status == types.ReceiptStatusFailed {
executionState.Status = StatusReverted
return executor.createFailedResult(executionState, fmt.Errorf("transaction reverted"))
}
executionState.Status = StatusConfirmed
// Calculate actual results
actualProfit, err := executor.calculateActualProfit(receipt, executionState.Opportunity)
if err != nil {
executor.logger.Warn(fmt.Sprintf("Failed to calculate actual profit: %v", err))
actualProfit = executionState.Opportunity.NetProfit // Use expected as fallback
}
executionState.ActualProfit = actualProfit
// Create successful result
return executor.createSuccessfulResult(executionState, receipt)
}
// submitTransaction submits the flash swap transaction
func (executor *FlashSwapExecutor) submitTransaction(
ctx context.Context,
flashSwapData *FlashSwapCalldata,
transactOpts *bind.TransactOpts,
) (*types.Transaction, error) {
// This is a simplified implementation
// Production would call the actual flash swap contract
executor.logger.Debug("Submitting flash swap transaction...")
executor.logger.Debug(fmt.Sprintf(" Initiator Pool: %s", flashSwapData.InitiatorPool.Hex()))
executor.logger.Debug(fmt.Sprintf(" Amount In: %s", flashSwapData.AmountIn.String()))
executor.logger.Debug(fmt.Sprintf(" Min Amount Out: %s", flashSwapData.MinAmountOut.String()))
executor.logger.Debug(fmt.Sprintf(" Token Path: %d tokens", len(flashSwapData.TokenPath)))
executor.logger.Debug(fmt.Sprintf(" Pool Path: %d pools", len(flashSwapData.Pools)))
// For demonstration, create a mock transaction
// Production would interact with actual contracts
// This is where we would actually call the flash swap contract method
// For now, we'll simulate creating a transaction that would call the flash swap function
// In production, you'd call the actual contract function like:
// tx, err := executor.flashSwapContract.FlashSwap(transactOpts, flashSwapData.InitiatorPool, ...)
// For this mock implementation, we'll return a transaction that would call the mock contract
nonce, err := executor.client.PendingNonceAt(context.Background(), transactOpts.From)
if err != nil {
nonce = 0 // fallback
}
// Create a mock transaction
tx := types.NewTransaction(
nonce,
flashSwapData.InitiatorPool, // Flash swap contract address
big.NewInt(0), // Value - no direct ETH transfer in flash swaps
transactOpts.GasLimit,
transactOpts.GasFeeCap,
flashSwapData.Data, // Encoded flash swap data
)
// In a real implementation, you'd need to sign and send the transaction
// For now, return a transaction object for the simulation
return tx, nil
}
// waitForConfirmation waits for transaction confirmation
func (executor *FlashSwapExecutor) waitForConfirmation(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
executor.logger.Debug(fmt.Sprintf("Waiting for confirmation of transaction: %s", txHash.Hex()))
// For demonstration, simulate a successful transaction
// Production would poll for actual transaction receipt
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for confirmation")
case <-time.After(3 * time.Second): // Simulate network delay
// Create mock receipt
receipt := &types.Receipt{
TxHash: txHash,
Status: types.ReceiptStatusSuccessful,
GasUsed: 750000,
EffectiveGasPrice: big.NewInt(100000000), // 0.1 gwei
BlockNumber: big.NewInt(1000000),
}
return receipt, nil
}
}
// calculateActualProfit calculates the actual profit from the transaction
func (executor *FlashSwapExecutor) calculateActualProfit(receipt *types.Receipt, opportunity *pkgtypes.ArbitrageOpportunity) (*math.UniversalDecimal, error) {
// Calculate actual gas cost
gasCost := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), receipt.EffectiveGasPrice)
gasCostDecimal, err := math.NewUniversalDecimal(gasCost, 18, "ETH")
if err != nil {
return nil, err
}
// For demonstration, assume we got the expected output
// Production would parse the transaction logs to get actual amounts
expectedOutput := opportunity.Profit
// Use the decimal converter to convert to ETH equivalent
// For simplicity, assume both input and output are already in compatible formats
// In real implementation, you'd need actual price data
netProfit, err := executor.decimalConverter.Subtract(expectedOutput, opportunity.AmountIn)
if err != nil {
return nil, err
}
// Subtract gas costs from net profit
netProfit, err = executor.decimalConverter.Subtract(netProfit, gasCostDecimal)
if err != nil {
return nil, err
}
return netProfit, nil
}
// createSuccessfulResult creates a successful execution result
func (executor *FlashSwapExecutor) createSuccessfulResult(state *ExecutionState, receipt *types.Receipt) *ExecutionResult {
// Convert UniversalDecimal to big.Int for ProfitRealized
profitRealized := big.NewInt(0)
if state.ActualProfit != nil {
profitRealized = state.ActualProfit
}
// Create a minimal ArbitragePath based on the opportunity
path := &ArbitragePath{
Tokens: []common.Address{state.Opportunity.TokenIn, state.Opportunity.TokenOut}, // Basic 2-token path
Pools: []*PoolInfo{}, // Empty for now
Protocols: []string{}, // Empty for now
Fees: []int64{}, // Empty for now
EstimatedGas: big.NewInt(0), // To be calculated
NetProfit: profitRealized,
ROI: 0, // To be calculated
LastUpdated: time.Now(),
}
gasCost := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), receipt.EffectiveGasPrice)
gasCostDecimal, _ := math.NewUniversalDecimal(gasCost, 18, "ETH")
return &ExecutionResult{
TransactionHash: state.TransactionHash,
GasUsed: receipt.GasUsed,
GasPrice: receipt.EffectiveGasPrice,
GasCost: gasCostDecimal,
ProfitRealized: profitRealized,
Success: true,
Error: nil,
ErrorMessage: "",
Status: "Success",
ExecutionTime: time.Since(state.StartTime),
Path: path,
}
}
// createFailedResult creates a failed execution result
func (executor *FlashSwapExecutor) createFailedResult(state *ExecutionState, err error) *ExecutionResult {
// Create a minimal ArbitragePath based on the opportunity
path := &ArbitragePath{
Tokens: []common.Address{state.Opportunity.TokenIn, state.Opportunity.TokenOut}, // Basic 2-token path
Pools: []*PoolInfo{}, // Empty for now
Protocols: []string{}, // Empty for now
Fees: []int64{}, // Empty for now
EstimatedGas: big.NewInt(0), // To be calculated
NetProfit: big.NewInt(0),
ROI: 0, // To be calculated
LastUpdated: time.Now(),
}
gasCostDecimal, _ := math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
return &ExecutionResult{
TransactionHash: state.TransactionHash,
GasUsed: 0,
GasPrice: big.NewInt(0),
GasCost: gasCostDecimal,
ProfitRealized: big.NewInt(0),
Success: false,
Error: err,
ErrorMessage: err.Error(),
Status: "Failed",
ExecutionTime: time.Since(state.StartTime),
Path: path,
}
}
// isRetryableError determines if an error is retryable
func (executor *FlashSwapExecutor) isRetryableError(errorMsg string) bool {
retryableErrors := []string{
"gas price too low",
"nonce too low",
"timeout",
"network error",
"connection refused",
"transaction underpriced",
"replacement transaction underpriced",
"known transaction",
}
for _, retryable := range retryableErrors {
if strings.Contains(strings.ToLower(errorMsg), strings.ToLower(retryable)) {
return true
}
}
return false
}
// updateGasPriceForRetry updates gas price for retry attempts
func (executor *FlashSwapExecutor) updateGasPriceForRetry(
ctx context.Context,
transactOpts *bind.TransactOpts,
attempt int,
) (*bind.TransactOpts, error) {
// Increase gas price by 20% for each retry
multiplier := 1.0 + float64(attempt)*0.2
if transactOpts.GasFeeCap != nil {
newGasFeeCap := new(big.Float).Mul(
new(big.Float).SetInt(transactOpts.GasFeeCap),
big.NewFloat(multiplier),
)
newGasFeeCapInt, _ := newGasFeeCap.Int(nil)
transactOpts.GasFeeCap = newGasFeeCapInt
}
if transactOpts.GasTipCap != nil {
newGasTipCap := new(big.Float).Mul(
new(big.Float).SetInt(transactOpts.GasTipCap),
big.NewFloat(multiplier),
)
newGasTipCapInt, _ := newGasTipCap.Int(nil)
transactOpts.GasTipCap = newGasTipCapInt
}
executor.logger.Debug(fmt.Sprintf("Updated gas prices for retry %d: MaxFee=%s, Priority=%s",
attempt,
transactOpts.GasFeeCap.String(),
transactOpts.GasTipCap.String()))
return transactOpts, nil
}
// updateExecutionStats updates execution statistics
func (executor *FlashSwapExecutor) updateExecutionStats(result *ExecutionResult) {
executor.executionHistory = append(executor.executionHistory, result)
if result.Success && result.ProfitRealized != nil {
profitDecimal, _ := math.NewUniversalDecimal(result.ProfitRealized, 18, "ETH")
executor.totalProfit, _ = executor.decimalConverter.Add(executor.totalProfit, profitDecimal)
}
if result.GasCost != nil {
executor.totalGasCost, _ = executor.decimalConverter.Add(executor.totalGasCost, result.GasCost)
}
// Clean up pending executions
delete(executor.pendingExecutions, result.TransactionHash)
// Keep only last 100 execution results
if len(executor.executionHistory) > 100 {
executor.executionHistory = executor.executionHistory[len(executor.executionHistory)-100:]
}
}
// GetExecutionStats returns execution statistics
func (executor *FlashSwapExecutor) GetExecutionStats() ExecutionStats {
successCount := 0
totalExecutions := len(executor.executionHistory)
for _, result := range executor.executionHistory {
if result.Success {
successCount++
}
}
successRate := 0.0
if totalExecutions > 0 {
successRate = float64(successCount) / float64(totalExecutions) * 100
}
return ExecutionStats{
TotalExecutions: totalExecutions,
SuccessfulExecutions: successCount,
SuccessRate: successRate,
TotalProfit: executor.totalProfit,
TotalGasCost: executor.totalGasCost,
PendingExecutions: len(executor.pendingExecutions),
}
}
// ExecutionStats contains execution statistics
type ExecutionStats struct {
TotalExecutions int
SuccessfulExecutions int
SuccessRate float64
TotalProfit *math.UniversalDecimal
TotalGasCost *math.UniversalDecimal
PendingExecutions int
}
// GetPendingExecutions returns currently pending executions
func (executor *FlashSwapExecutor) GetPendingExecutions() map[common.Hash]*ExecutionState {
return executor.pendingExecutions
}
// GetExecutionHistory returns recent execution history
func (executor *FlashSwapExecutor) GetExecutionHistory(limit int) []*ExecutionResult {
if limit <= 0 || limit > len(executor.executionHistory) {
limit = len(executor.executionHistory)
}
start := len(executor.executionHistory) - limit
return executor.executionHistory[start:]
}