Files
mev-beta/orig/pkg/arbitrage/flash_executor.go
Administrator 803de231ba 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>
2025-11-10 10:14:26 +01:00

1464 lines
45 KiB
Go

package arbitrage
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
stdmath "math"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/fraktal/mev-beta/bindings/contracts"
"github.com/fraktal/mev-beta/bindings/flashswap"
"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
// Contract bindings
flashSwapBinding *flashswap.BaseFlashSwapper
arbitrageBinding arbitrageLogParser
// Configuration
config ExecutionConfig
// State tracking
pendingExecutions map[common.Hash]*ExecutionState
executionHistory []*ExecutionResult
totalProfit *math.UniversalDecimal
totalGasCost *math.UniversalDecimal
// Token metadata helpers
tokenRegistry map[common.Address]tokenDescriptor
ethReferenceToken common.Address
}
// 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
}
type arbitrageLogParser interface {
ParseArbitrageExecuted(types.Log) (*contracts.ArbitrageExecutorArbitrageExecuted, error)
}
type tokenDescriptor struct {
Symbol string
Decimals uint8
PriceUSD *big.Rat
}
var (
revertErrorSelector = []byte{0x08, 0xc3, 0x79, 0xa0} // keccak256("Error(string)")[:4]
revertPanicSelector = []byte{0x4e, 0x48, 0x7b, 0x71} // keccak256("Panic(uint256)")[:4]
)
// 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),
tokenRegistry: defaultTokenRegistry(),
ethReferenceToken: common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
}
if client != nil && flashSwapContract != (common.Address{}) {
if binding, err := flashswap.NewBaseFlashSwapper(flashSwapContract, client); err != nil {
if logger != nil {
logger.Warn(fmt.Sprintf("Failed to initialise flash swap contract binding: %v", err))
}
} else {
executor.flashSwapBinding = binding
}
}
if client != nil && arbitrageContract != (common.Address{}) {
if binding, err := contracts.NewArbitrageExecutor(arbitrageContract, client); err != nil {
if logger != nil {
logger.Warn(fmt.Sprintf("Failed to initialise arbitrage contract binding: %v", err))
}
} else {
executor.arbitrageBinding = binding
}
}
// 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) {
// Validate opportunity before execution
if err := executor.validateOpportunity(opportunity); err != nil {
return nil, fmt.Errorf("opportunity validation failed: %w", err)
}
profitEstimate, err := executor.profitEstimateWei(opportunity)
if err != nil {
return nil, fmt.Errorf("unable to determine profit estimate: %w", err)
}
profitDisplay := ethAmountString(executor.decimalConverter, nil, profitEstimate)
executor.logger.Info(fmt.Sprintf("🚀 Executing arbitrage opportunity: %s ETH profit expected",
profitDisplay))
// 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
profitDisplay := ethAmountString(executor.decimalConverter, nil, result.ProfitRealized)
executor.logger.Info(fmt.Sprintf("💰 Actual profit: %s ETH",
profitDisplay))
}
return result, nil
}
// validateOpportunity validates an opportunity before execution
func (executor *FlashSwapExecutor) validateOpportunity(opportunity *pkgtypes.ArbitrageOpportunity) error {
if opportunity == nil {
return fmt.Errorf("opportunity cannot be nil")
}
if opportunity.AmountIn == nil || opportunity.AmountIn.Sign() <= 0 {
return fmt.Errorf("invalid amount in for opportunity")
}
netProfit, err := executor.profitEstimateWei(opportunity)
if err != nil {
return fmt.Errorf("profit estimate invalid: %w", err)
}
// Check minimum profit threshold
// Set to 0.001 ETH to ensure profitability after gas costs
// Arbitrum gas cost: ~100k-200k gas @ 0.1-0.2 gwei = ~0.00002-0.00004 ETH
// 0.001 ETH provides ~25-50x gas cost safety margin
minProfitWei := big.NewInt(1000000000000000) // 0.001 ETH in wei
if netProfit.Cmp(minProfitWei) < 0 {
return fmt.Errorf("profit %s below minimum threshold %s",
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 opportunity == nil {
return nil, fmt.Errorf("opportunity cannot be nil")
}
if len(opportunity.Path) < 2 {
return nil, fmt.Errorf("path must have at least 2 tokens")
}
if opportunity.AmountIn == nil || opportunity.AmountIn.Sign() <= 0 {
return nil, fmt.Errorf("opportunity amount in must be positive")
}
// Convert path strings to token addresses
tokenPath := make([]common.Address, len(opportunity.Path))
for i, tokenAddr := range opportunity.Path {
tokenPath[i] = common.HexToAddress(tokenAddr)
}
// Use pool addresses from opportunity if available
poolAddresses := make([]common.Address, len(opportunity.Pools))
for i, poolAddr := range opportunity.Pools {
poolAddresses[i] = common.HexToAddress(poolAddr)
}
if len(poolAddresses) == 0 {
return nil, fmt.Errorf("opportunity missing pool data")
}
profitEstimate, err := executor.profitEstimateWei(opportunity)
if err != nil {
return nil, err
}
expectedOutput := new(big.Int).Add(opportunity.AmountIn, profitEstimate)
if expectedOutput.Sign() <= 0 {
return nil, fmt.Errorf("expected output amount must be positive")
}
slippageFraction := executor.resolveSlippage(opportunity)
if slippageFraction < 0 {
slippageFraction = 0
}
if slippageFraction >= 1 {
slippageFraction = 0.99
}
minAmountOut := new(big.Int).Set(expectedOutput)
if slippageFraction > 0 {
minOutputFloat := new(big.Float).Mul(new(big.Float).SetInt(expectedOutput), big.NewFloat(1-slippageFraction))
minAmountOut = new(big.Int)
minOutputFloat.Int(minAmountOut)
}
if minAmountOut.Sign() <= 0 {
return nil, fmt.Errorf("minimum amount out must be positive")
}
calldata := &FlashSwapCalldata{
InitiatorPool: poolAddresses[0], // First pool initiates the flash swap
TokenPath: tokenPath,
Pools: poolAddresses,
AmountIn: new(big.Int).Set(opportunity.AmountIn),
MinAmountOut: minAmountOut,
Recipient: executor.arbitrageContract, // Arbitrage contract receives callback
Data: executor.encodeArbitrageData(tokenPath, poolAddresses, nil, minAmountOut),
}
return calldata, nil
}
// encodeArbitrageData encodes the arbitrage execution data
func (executor *FlashSwapExecutor) encodeArbitrageData(tokenPath, poolPath []common.Address, fees []*big.Int, minAmountOut *big.Int) []byte {
payload, err := encodeFlashSwapCallback(tokenPath, poolPath, fees, minAmountOut)
if err != nil {
if executor.logger != nil {
executor.logger.Warn(fmt.Sprintf("Failed to encode flash swap callback data: %v", err))
}
// Provide a structured fallback payload for debugging purposes
fallback := []string{"arbitrage"}
for _, token := range tokenPath {
fallback = append(fallback, token.Hex())
}
return []byte(strings.Join(fallback, ":"))
}
return payload
}
func (executor *FlashSwapExecutor) profitEstimateWei(opportunity *pkgtypes.ArbitrageOpportunity) (*big.Int, error) {
if opportunity == nil {
return nil, fmt.Errorf("opportunity cannot be nil")
}
candidates := []*big.Int{
opportunity.NetProfit,
opportunity.Profit,
opportunity.EstimatedProfit,
}
for _, candidate := range candidates {
if candidate != nil && candidate.Sign() > 0 {
return new(big.Int).Set(candidate), nil
}
}
return nil, fmt.Errorf("opportunity profit estimate unavailable or non-positive")
}
func (executor *FlashSwapExecutor) resolveSlippage(opportunity *pkgtypes.ArbitrageOpportunity) float64 {
if opportunity != nil && opportunity.MaxSlippage > 0 {
if opportunity.MaxSlippage > 1 {
return opportunity.MaxSlippage / 100.0
}
return opportunity.MaxSlippage
}
if executor.config.MaxSlippage != nil && executor.config.MaxSlippage.Value != nil {
numerator := new(big.Float).SetInt(executor.config.MaxSlippage.Value)
denominator := big.NewFloat(stdmath.Pow10(int(executor.config.MaxSlippage.Decimals)))
if denominator.Sign() != 0 {
percent, _ := new(big.Float).Quo(numerator, denominator).Float64()
if percent > 0 {
return percent / 100.0
}
}
}
return 0.01 // Default to 1% if nothing provided
}
func (executor *FlashSwapExecutor) gasEstimateWei(opportunity *pkgtypes.ArbitrageOpportunity) *big.Int {
if opportunity == nil {
return nil
}
if opportunity.GasEstimate != nil {
return new(big.Int).Set(opportunity.GasEstimate)
}
if opportunity.GasCost != nil {
return new(big.Int).Set(opportunity.GasCost)
}
return nil
}
// getTransactionOptions prepares transaction options with dynamic gas pricing
func (executor *FlashSwapExecutor) getTransactionOptions(ctx context.Context, flashSwapData *FlashSwapCalldata) (*bind.TransactOpts, error) {
if executor.client == nil {
return nil, fmt.Errorf("ethereum client not configured")
}
if executor.keyManager == nil {
return nil, fmt.Errorf("key manager not configured")
}
if flashSwapData == nil {
return nil, fmt.Errorf("flash swap data cannot be nil")
}
privateKey, err := executor.keyManager.GetActivePrivateKey()
if err != nil {
return nil, fmt.Errorf("failed to get private key: %w", err)
}
chainID, err := executor.client.ChainID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
}
transactOpts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID)
if err != nil {
return nil, fmt.Errorf("failed to create transactor: %w", err)
}
nonce, err := executor.client.PendingNonceAt(ctx, transactOpts.From)
if err != nil {
return nil, fmt.Errorf("failed to fetch account nonce: %w", err)
}
transactOpts.Nonce = new(big.Int).SetUint64(nonce)
params, err := executor.buildFlashSwapParams(flashSwapData)
if err != nil {
return nil, err
}
callData, err := executor.encodeFlashSwapCall(flashSwapData.Pools[0], params)
if err != nil {
return nil, fmt.Errorf("failed to encode flash swap calldata: %w", err)
}
gasLimit := uint64(800000) // Sensible default for complex flash swaps
var candidateFeeCap *big.Int
var candidateTip *big.Int
if executor.flashSwapContract == (common.Address{}) {
executor.logger.Warn("Flash swap contract address not configured; using default gas limit")
} else {
callMsg := ethereum.CallMsg{
From: transactOpts.From,
To: &executor.flashSwapContract,
Data: callData,
Value: func() *big.Int {
if transactOpts.Value == nil {
return big.NewInt(0)
}
return transactOpts.Value
}(),
}
if estimatedGas, gasErr := executor.client.EstimateGas(ctx, callMsg); gasErr == nil && estimatedGas > 0 {
gasLimit = estimatedGas
} else {
if gasErr != nil {
executor.logger.Warn(fmt.Sprintf("Gas estimation via RPC failed: %v", gasErr))
}
if executor.gasEstimator != nil {
dummyTx := types.NewTx(&types.DynamicFeeTx{
ChainID: chainID,
Nonce: nonce,
To: &executor.flashSwapContract,
Value: callMsg.Value,
Data: callData,
})
if estimate, estErr := executor.gasEstimator.EstimateL2Gas(ctx, dummyTx); estErr == nil {
if estimate.GasLimit > 0 {
gasLimit = estimate.GasLimit
}
candidateFeeCap = estimate.MaxFeePerGas
candidateTip = estimate.MaxPriorityFee
} else {
executor.logger.Warn(fmt.Sprintf("Arbitrum gas estimator fallback failed: %v", estErr))
}
}
}
}
multiplier := executor.config.GasLimitMultiplier
if multiplier <= 0 {
multiplier = 1.2
}
adjustedGasLimit := gasLimit
if multiplier != 1.0 {
adjusted := uint64(stdmath.Ceil(float64(gasLimit) * multiplier))
if adjusted < gasLimit {
adjusted = gasLimit
}
if adjusted == 0 {
adjusted = gasLimit
}
adjustedGasLimit = adjusted
}
if adjustedGasLimit == 0 {
adjustedGasLimit = gasLimit
}
transactOpts.GasLimit = adjustedGasLimit
if candidateTip == nil {
if suggestedTip, tipErr := executor.client.SuggestGasTipCap(ctx); tipErr == nil {
candidateTip = suggestedTip
} else {
candidateTip = big.NewInt(100000000) // 0.1 gwei fallback
executor.logger.Debug(fmt.Sprintf("Using fallback priority fee: %s", candidateTip.String()))
}
}
if candidateFeeCap == nil {
if header, headerErr := executor.client.HeaderByNumber(ctx, nil); headerErr == nil && header != nil && header.BaseFee != nil {
candidateFeeCap = new(big.Int).Add(new(big.Int).Mul(header.BaseFee, big.NewInt(2)), candidateTip)
} else {
defaultBase := big.NewInt(1000000000) // 1 gwei
candidateFeeCap = new(big.Int).Add(defaultBase, candidateTip)
}
}
transactOpts.GasTipCap = new(big.Int).Set(candidateTip)
transactOpts.GasFeeCap = new(big.Int).Set(candidateFeeCap)
baseStr := fmt.Sprintf("%d", gasLimit)
maxFeeStr := "<nil>"
tipStr := "<nil>"
if transactOpts.GasFeeCap != nil {
maxFeeStr = transactOpts.GasFeeCap.String()
}
if transactOpts.GasTipCap != nil {
tipStr = transactOpts.GasTipCap.String()
}
executor.logger.Debug(fmt.Sprintf("Gas estimate - Base: %s, Adjusted: %d, MaxFee: %s, Priority: %s",
baseStr,
transactOpts.GasLimit,
maxFeeStr,
tipStr))
// Apply priority fee strategy and enforce configured limits
executor.applyPriorityFeeStrategy(transactOpts)
executor.enforceGasBounds(transactOpts)
return transactOpts, nil
}
func (executor *FlashSwapExecutor) buildFlashSwapParams(flashSwapData *FlashSwapCalldata) (flashswap.FlashSwapParams, error) {
if flashSwapData == nil {
return flashswap.FlashSwapParams{}, fmt.Errorf("flash swap data cannot be nil")
}
if len(flashSwapData.TokenPath) < 2 {
return flashswap.FlashSwapParams{}, fmt.Errorf("token path must include at least two tokens")
}
if len(flashSwapData.Pools) == 0 {
return flashswap.FlashSwapParams{}, fmt.Errorf("pool path cannot be empty")
}
if flashSwapData.AmountIn == nil || flashSwapData.AmountIn.Sign() <= 0 {
return flashswap.FlashSwapParams{}, fmt.Errorf("amount in must be positive")
}
if flashSwapData.MinAmountOut == nil || flashSwapData.MinAmountOut.Sign() <= 0 {
return flashswap.FlashSwapParams{}, fmt.Errorf("minimum amount out must be positive")
}
params := flashswap.FlashSwapParams{
Token0: flashSwapData.TokenPath[0],
Token1: flashSwapData.TokenPath[1],
Amount0: flashSwapData.AmountIn,
Amount1: big.NewInt(0),
To: executor.arbitrageContract,
Data: flashSwapData.Data,
}
return params, nil
}
func (executor *FlashSwapExecutor) encodeFlashSwapCall(pool common.Address, params flashswap.FlashSwapParams) ([]byte, error) {
if pool == (common.Address{}) {
return nil, fmt.Errorf("pool address cannot be zero")
}
flashSwapABI, err := flashswap.BaseFlashSwapperMetaData.GetAbi()
if err != nil {
return nil, fmt.Errorf("failed to load BaseFlashSwapper ABI: %w", err)
}
return flashSwapABI.Pack("executeFlashSwap", pool, params)
}
func (executor *FlashSwapExecutor) maxGasPriceWei() *big.Int {
if executor.config.MaxGasPrice == nil || executor.config.MaxGasPrice.Value == nil {
return nil
}
if executor.decimalConverter == nil {
return new(big.Int).Set(executor.config.MaxGasPrice.Value)
}
weiDecimal := executor.decimalConverter.ToWei(executor.config.MaxGasPrice)
if weiDecimal == nil || weiDecimal.Value == nil {
return new(big.Int).Set(executor.config.MaxGasPrice.Value)
}
return new(big.Int).Set(weiDecimal.Value)
}
func (executor *FlashSwapExecutor) enforceGasBounds(transactOpts *bind.TransactOpts) {
if transactOpts == nil {
return
}
maxGas := executor.maxGasPriceWei()
if maxGas == nil || maxGas.Sign() <= 0 {
return
}
if transactOpts.GasFeeCap != nil && transactOpts.GasFeeCap.Cmp(maxGas) > 0 {
transactOpts.GasFeeCap = new(big.Int).Set(maxGas)
executor.logger.Debug(fmt.Sprintf("Clamped gas fee cap to configured maximum %s", maxGas.String()))
}
if transactOpts.GasTipCap != nil && transactOpts.GasTipCap.Cmp(maxGas) > 0 {
transactOpts.GasTipCap = new(big.Int).Set(maxGas)
executor.logger.Debug(fmt.Sprintf("Clamped priority fee to configured maximum %s", maxGas.String()))
}
if transactOpts.GasFeeCap != nil && transactOpts.GasTipCap != nil && transactOpts.GasFeeCap.Cmp(transactOpts.GasTipCap) < 0 {
transactOpts.GasFeeCap = new(big.Int).Set(transactOpts.GasTipCap)
}
}
// 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)
}
}
// 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
revertReason := executor.fetchRevertReason(timeoutCtx, tx.Hash(), receipt)
if revertReason != "" {
return executor.createFailedResult(executionState, fmt.Errorf("transaction reverted: %s", revertReason))
}
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))
if estimate, estimateErr := executor.profitEstimateWei(executionState.Opportunity); estimateErr == nil {
actualProfit = universalFromWei(executor.decimalConverter, estimate, "ETH")
} else {
actualProfit, _ = math.NewUniversalDecimal(big.NewInt(0), 18, "ETH")
}
}
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) {
if executor.flashSwapBinding == nil {
return nil, fmt.Errorf("flash swap contract binding not initialised")
}
if executor.arbitrageContract == (common.Address{}) {
return nil, fmt.Errorf("arbitrage contract address not configured")
}
params, err := executor.buildFlashSwapParams(flashSwapData)
if err != nil {
return nil, err
}
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)))
opts := *transactOpts
opts.Context = ctx
tx, err := executor.flashSwapBinding.ExecuteFlashSwap(&opts, flashSwapData.Pools[0], params)
if err != nil {
return nil, err
}
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()))
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for confirmation: %w", ctx.Err())
case <-ticker.C:
receipt, err := executor.client.TransactionReceipt(ctx, txHash)
if err != nil {
if errors.Is(err, ethereum.NotFound) {
continue
}
return nil, fmt.Errorf("failed to fetch transaction receipt: %w", err)
}
if receipt == nil {
continue
}
if receipt.BlockNumber == nil || executor.config.ConfirmationBlocks <= 1 {
return receipt, nil
}
targetBlock := new(big.Int).Add(receipt.BlockNumber, big.NewInt(int64(executor.config.ConfirmationBlocks-1)))
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("timeout waiting for confirmations: %w", ctx.Err())
case <-ticker.C:
header, headerErr := executor.client.HeaderByNumber(ctx, nil)
if headerErr != nil {
if errors.Is(headerErr, ethereum.NotFound) {
continue
}
return nil, fmt.Errorf("failed to fetch latest block header: %w", headerErr)
}
if header != nil && header.Number.Cmp(targetBlock) >= 0 {
return receipt, nil
}
}
}
}
}
}
func (executor *FlashSwapExecutor) fetchRevertReason(ctx context.Context, txHash common.Hash, receipt *types.Receipt) string {
if executor.client == nil || receipt == nil {
return ""
}
callCtx := ctx
if callCtx == nil || callCtx.Err() != nil {
var cancel context.CancelFunc
callCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
}
tx, _, err := executor.client.TransactionByHash(callCtx, txHash)
if err != nil || tx == nil {
if err != nil && executor.logger != nil {
executor.logger.Debug(fmt.Sprintf("Failed to fetch transaction for revert reason: %v", err))
}
return ""
}
if tx.To() == nil {
return ""
}
msg := ethereum.CallMsg{
To: tx.To(),
Data: tx.Data(),
Gas: tx.Gas(),
Value: tx.Value(),
}
switch tx.Type() {
case types.DynamicFeeTxType:
msg.GasFeeCap = tx.GasFeeCap()
msg.GasTipCap = tx.GasTipCap()
default:
msg.GasPrice = tx.GasPrice()
}
revertData, callErr := executor.client.CallContract(callCtx, msg, receipt.BlockNumber)
if callErr != nil || len(revertData) == 0 {
if callErr != nil && executor.logger != nil {
executor.logger.Debug(fmt.Sprintf("Failed to retrieve revert data: %v", callErr))
}
return ""
}
reason := parseRevertReason(revertData)
if reason == "" {
return fmt.Sprintf("0x%s", hex.EncodeToString(revertData))
}
return reason
}
func parseRevertReason(data []byte) string {
if len(data) < 4 {
return ""
}
selector := data[:4]
payload := data[4:]
switch {
case bytes.Equal(selector, revertErrorSelector):
strType, err := abi.NewType("string", "", nil)
if err != nil {
return ""
}
args := abi.Arguments{{Type: strType}}
values, err := args.Unpack(payload)
if err != nil || len(values) == 0 {
return ""
}
if reason, ok := values[0].(string); ok {
return reason
}
case bytes.Equal(selector, revertPanicSelector):
uintType, err := abi.NewType("uint256", "", nil)
if err != nil {
return ""
}
args := abi.Arguments{{Type: uintType}}
values, err := args.Unpack(payload)
if err != nil || len(values) == 0 {
return ""
}
if code, ok := values[0].(*big.Int); ok {
return fmt.Sprintf("panic code 0x%s", strings.ToLower(code.Text(16)))
}
default:
// Some contracts return raw reason strings without selector
if utf8String := extractUTF8String(data); utf8String != "" {
return utf8String
}
}
return ""
}
func extractUTF8String(data []byte) string {
if len(data) == 0 {
return ""
}
trimmed := bytes.Trim(data, "\x00")
if len(trimmed) == 0 {
return ""
}
for _, b := range trimmed {
// Allow printable ASCII range plus common whitespace
if (b < 0x20 || b > 0x7E) && b != 0x0a && b != 0x0d && b != 0x09 {
return ""
}
}
return string(trimmed)
}
// calculateActualProfit calculates the actual profit from the transaction
func (executor *FlashSwapExecutor) calculateActualProfit(receipt *types.Receipt, opportunity *pkgtypes.ArbitrageOpportunity) (*math.UniversalDecimal, error) {
if receipt == nil {
return nil, fmt.Errorf("transaction receipt cannot be nil")
}
gasCostWei := executor.calculateGasCostWei(receipt)
var parsedEvent *contracts.ArbitrageExecutorArbitrageExecuted
if executor.arbitrageBinding != nil {
for _, log := range receipt.Logs {
if log.Address != executor.arbitrageContract {
continue
}
event, err := executor.arbitrageBinding.ParseArbitrageExecuted(*log)
if err != nil {
if executor.logger != nil {
executor.logger.Debug("Failed to parse arbitrage execution log", "error", err)
}
continue
}
parsedEvent = event
break
}
}
profitAmount := executor.extractProfitAmount(parsedEvent, opportunity)
if profitAmount == nil {
return nil, fmt.Errorf("unable to determine profit from receipt or opportunity")
}
profitToken, descriptor := executor.resolveProfitDescriptor(parsedEvent, opportunity)
gasCostInToken := executor.convertGasCostToTokenUnits(gasCostWei, profitToken, descriptor)
netProfit := new(big.Int).Set(profitAmount)
if gasCostInToken != nil {
netProfit.Sub(netProfit, gasCostInToken)
}
actualProfitDecimal, err := math.NewUniversalDecimal(netProfit, descriptor.Decimals, descriptor.Symbol)
if err != nil {
return nil, err
}
return actualProfitDecimal, nil
}
func (executor *FlashSwapExecutor) calculateGasCostWei(receipt *types.Receipt) *big.Int {
if receipt == nil {
return big.NewInt(0)
}
gasUsedBigInt := new(big.Int).SetUint64(receipt.GasUsed)
gasPrice := receipt.EffectiveGasPrice
if gasPrice == nil {
gasPrice = big.NewInt(0)
}
return new(big.Int).Mul(gasUsedBigInt, gasPrice)
}
func (executor *FlashSwapExecutor) extractProfitAmount(event *contracts.ArbitrageExecutorArbitrageExecuted, opportunity *pkgtypes.ArbitrageOpportunity) *big.Int {
if event != nil && event.Profit != nil {
return new(big.Int).Set(event.Profit)
}
if opportunity == nil {
return nil
}
if opportunity.NetProfit != nil && opportunity.NetProfit.Sign() > 0 {
return new(big.Int).Set(opportunity.NetProfit)
}
if estimate, err := executor.profitEstimateWei(opportunity); err == nil {
return estimate
}
return nil
}
func (executor *FlashSwapExecutor) resolveProfitDescriptor(event *contracts.ArbitrageExecutorArbitrageExecuted, opportunity *pkgtypes.ArbitrageOpportunity) (common.Address, tokenDescriptor) {
var tokenAddr common.Address
if event != nil && len(event.Tokens) > 0 {
tokenAddr = event.Tokens[len(event.Tokens)-1]
} else if opportunity != nil && opportunity.TokenOut != (common.Address{}) {
tokenAddr = opportunity.TokenOut
}
descriptor, ok := executor.tokenRegistry[tokenAddr]
if !ok {
if opportunity != nil && opportunity.Quantities != nil {
descriptor.Symbol = opportunity.Quantities.NetProfit.Symbol
descriptor.Decimals = opportunity.Quantities.NetProfit.Decimals
}
if descriptor.Symbol == "" {
descriptor.Symbol = "ETH"
}
if descriptor.Decimals == 0 {
descriptor.Decimals = 18
}
} else {
// Copy to avoid mutating registry entry
descriptor = tokenDescriptor{
Symbol: descriptor.Symbol,
Decimals: descriptor.Decimals,
PriceUSD: descriptor.PriceUSD,
}
}
return tokenAddr, descriptor
}
func (executor *FlashSwapExecutor) convertGasCostToTokenUnits(gasCostWei *big.Int, tokenAddr common.Address, descriptor tokenDescriptor) *big.Int {
if gasCostWei == nil || gasCostWei.Sign() == 0 {
return big.NewInt(0)
}
if tokenAddr == (common.Address{}) || tokenAddr == executor.ethReferenceToken || strings.EqualFold(descriptor.Symbol, "ETH") || strings.EqualFold(descriptor.Symbol, "WETH") {
return new(big.Int).Set(gasCostWei)
}
if descriptor.PriceUSD == nil {
if executor.logger != nil {
executor.logger.Debug("Gas cost conversion skipped due to missing price data", "token", descriptor.Symbol)
}
return nil
}
ethDescriptor, ok := executor.tokenRegistry[executor.ethReferenceToken]
if !ok || ethDescriptor.PriceUSD == nil {
if executor.logger != nil {
executor.logger.Debug("Gas cost conversion skipped due to missing ETH pricing")
}
return nil
}
numerator := new(big.Rat).SetInt(gasCostWei)
numerator.Mul(numerator, ethDescriptor.PriceUSD)
denominator := new(big.Rat).Mul(new(big.Rat).SetInt(powerOfTenInt(18)), descriptor.PriceUSD)
if denominator.Sign() == 0 {
return nil
}
tokenAmount := new(big.Rat).Quo(numerator, denominator)
tokenAmount.Mul(tokenAmount, new(big.Rat).SetInt(powerOfTenUint(descriptor.Decimals)))
// Floor conversion to avoid overstating deductions
result := new(big.Int).Quo(tokenAmount.Num(), tokenAmount.Denom())
if result.Sign() == 0 && tokenAmount.Sign() > 0 {
// Ensure non-zero deduction when value exists to avoid under-accounting
result = big.NewInt(1)
}
return result
}
func defaultTokenRegistry() map[common.Address]tokenDescriptor {
registry := map[common.Address]tokenDescriptor{
{}: newTokenDescriptor("ETH", 18, 2000.0),
common.HexToAddress("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"): newTokenDescriptor("WETH", 18, 2000.0),
common.HexToAddress("0xaF88d065e77c8cC2239327C5EDb3A432268e5831"): newTokenDescriptor("USDC", 6, 1.0),
common.HexToAddress("0xff970a61a04b1ca14834a43f5de4533ebddb5cc8"): newTokenDescriptor("USDC.e", 6, 1.0),
common.HexToAddress("0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"): newTokenDescriptor("USDT", 6, 1.0),
common.HexToAddress("0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f"): newTokenDescriptor("WBTC", 8, 43000.0),
common.HexToAddress("0x912CE59144191C1204E64559FE8253a0e49E6548"): newTokenDescriptor("ARB", 18, 0.75),
common.HexToAddress("0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a"): newTokenDescriptor("GMX", 18, 45.0),
common.HexToAddress("0xf97f4df75117a78c1a5a0dbb814af92458539fb4"): newTokenDescriptor("LINK", 18, 12.0),
common.HexToAddress("0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0"): newTokenDescriptor("UNI", 18, 8.0),
common.HexToAddress("0xba5ddd1f9d7f570dc94a51479a000e3bce967196"): newTokenDescriptor("AAVE", 18, 85.0),
}
return registry
}
func newTokenDescriptor(symbol string, decimals uint8, price float64) tokenDescriptor {
desc := tokenDescriptor{
Symbol: symbol,
Decimals: decimals,
}
if price > 0 {
desc.PriceUSD = new(big.Rat).SetFloat64(price)
}
return desc
}
func powerOfTenInt(exp int) *big.Int {
return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), nil)
}
func powerOfTenUint(exp uint8) *big.Int {
return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(exp)), 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 = new(big.Int).Set(state.ActualProfit.Value)
} else if state.Opportunity != nil && state.Opportunity.NetProfit != nil {
profitRealized = new(big.Int).Set(state.Opportunity.NetProfit)
} else if estimate, err := executor.profitEstimateWei(state.Opportunity); err == nil {
profitRealized = estimate
}
// 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(),
}
gasUsedBigInt := new(big.Int).SetUint64(receipt.GasUsed)
gasCost := new(big.Int).Mul(gasUsedBigInt, 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:]
}