817 lines
27 KiB
Go
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:]
|
|
}
|