feat: create v2-prep branch with comprehensive planning

Restructured project for V2 refactor:

**Structure Changes:**
- Moved all V1 code to orig/ folder (preserved with git mv)
- Created docs/planning/ directory
- Added orig/README_V1.md explaining V1 preservation

**Planning Documents:**
- 00_V2_MASTER_PLAN.md: Complete architecture overview
  - Executive summary of critical V1 issues
  - High-level component architecture diagrams
  - 5-phase implementation roadmap
  - Success metrics and risk mitigation

- 07_TASK_BREAKDOWN.md: Atomic task breakdown
  - 99+ hours of detailed tasks
  - Every task < 2 hours (atomic)
  - Clear dependencies and success criteria
  - Organized by implementation phase

**V2 Key Improvements:**
- Per-exchange parsers (factory pattern)
- Multi-layer strict validation
- Multi-index pool cache
- Background validation pipeline
- Comprehensive observability

**Critical Issues Addressed:**
- Zero address tokens (strict validation + cache enrichment)
- Parsing accuracy (protocol-specific parsers)
- No audit trail (background validation channel)
- Inefficient lookups (multi-index cache)
- Stats disconnection (event-driven metrics)

Next Steps:
1. Review planning documents
2. Begin Phase 1: Foundation (P1-001 through P1-010)
3. Implement parsers in Phase 2
4. Build cache system in Phase 3
5. Add validation pipeline in Phase 4
6. Migrate and test in Phase 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Administrator
2025-11-10 10:14:26 +01:00
parent 1773daffe7
commit 803de231ba
411 changed files with 20390 additions and 8680 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
package parser
import (
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/fraktal/mev-beta/pkg/calldata"
)
type multicallFixture struct {
TxHash string `json:"tx_hash"`
Protocol string `json:"protocol"`
CallData string `json:"call_data"`
}
func TestDecodeUniswapV3MulticallFixture(t *testing.T) {
fixturePath := filepath.Join("..", "..", "..", "test", "fixtures", "multicall_samples", "uniswap_v3_usdc_weth.json")
data, err := os.ReadFile(fixturePath)
require.NoError(t, err, "failed to read fixture %s", fixturePath)
var fx multicallFixture
require.NoError(t, json.Unmarshal(data, &fx))
require.NotEmpty(t, fx.CallData)
hexData := strings.TrimPrefix(fx.CallData, "0x")
payload, err := hex.DecodeString(hexData)
require.NoError(t, err)
decoder, err := NewABIDecoder()
require.NoError(t, err)
rawSwap, err := decoder.DecodeSwapTransaction(fx.Protocol, payload)
require.NoError(t, err)
swap, ok := rawSwap.(*SwapEvent)
require.True(t, ok, "expected SwapEvent from fixture decode")
require.Equal(t, "0xaf88d065e77c8cc2239327c5edb3a432268e5831", strings.ToLower(swap.TokenIn.Hex()))
require.Equal(t, "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", strings.ToLower(swap.TokenOut.Hex()))
require.NotEmpty(t, fx.TxHash, "fixture should include tx hash reference for external verification")
}
func TestDecodeDiagnosticMulticallFixture(t *testing.T) {
fixturePath := filepath.Join("..", "..", "..", "test", "fixtures", "multicall_samples", "diagnostic_zero_addresses.json")
data, err := os.ReadFile(fixturePath)
require.NoError(t, err, "failed to read fixture %s", fixturePath)
var fx multicallFixture
require.NoError(t, json.Unmarshal(data, &fx))
require.NotEmpty(t, fx.CallData)
hexData := strings.TrimPrefix(fx.CallData, "0x")
payload, err := hex.DecodeString(hexData)
require.NoError(t, err)
ctx := &calldata.MulticallContext{TxHash: fx.TxHash, Protocol: fx.Protocol, Stage: "fixture-test"}
tokens, err := calldata.ExtractTokensFromMulticallWithContext(payload, ctx)
// With our new validation system, this should now return an error for corrupted data
if err != nil {
require.Error(t, err, "diagnostic fixture with zero addresses should return error due to enhanced validation")
require.Contains(t, err.Error(), "no tokens extracted", "error should indicate no valid tokens found")
} else {
require.Len(t, tokens, 0, "if no error, should yield no valid tokens")
}
}

View File

@@ -0,0 +1,96 @@
package parser
import (
"math/big"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
const uniswapV3RouterABI = `[
{
"name":"multicall",
"type":"function",
"stateMutability":"payable",
"inputs":[
{"name":"deadline","type":"uint256"},
{"name":"data","type":"bytes[]"}
],
"outputs":[]
},
{
"name":"exactInputSingle",
"type":"function",
"stateMutability":"payable",
"inputs":[
{
"name":"params",
"type":"tuple",
"components":[
{"name":"tokenIn","type":"address"},
{"name":"tokenOut","type":"address"},
{"name":"fee","type":"uint24"},
{"name":"recipient","type":"address"},
{"name":"deadline","type":"uint256"},
{"name":"amountIn","type":"uint256"},
{"name":"amountOutMinimum","type":"uint256"},
{"name":"sqrtPriceLimitX96","type":"uint160"}
]
}
],
"outputs":[{"name":"","type":"uint256"}]
}
]`
type exactInputSingleParams struct {
TokenIn common.Address `abi:"tokenIn"`
TokenOut common.Address `abi:"tokenOut"`
Fee *big.Int `abi:"fee"`
Recipient common.Address `abi:"recipient"`
Deadline *big.Int `abi:"deadline"`
AmountIn *big.Int `abi:"amountIn"`
AmountOutMinimum *big.Int `abi:"amountOutMinimum"`
SqrtPriceLimitX96 *big.Int `abi:"sqrtPriceLimitX96"`
}
func TestDecodeUniswapV3Multicall(t *testing.T) {
decoder, err := NewABIDecoder()
require.NoError(t, err)
routerABI, err := abi.JSON(strings.NewReader(uniswapV3RouterABI))
require.NoError(t, err)
tokenIn := common.HexToAddress("0xaf88d065e77c8cc2239327c5edb3a432268e5831")
tokenOut := common.HexToAddress("0x82af49447d8a07e3bd95bd0d56f35241523fbab1")
params := exactInputSingleParams{
TokenIn: tokenIn,
TokenOut: tokenOut,
Fee: big.NewInt(500),
Recipient: common.HexToAddress("0x1111111254eeb25477b68fb85ed929f73a960582"),
Deadline: big.NewInt(0),
AmountIn: big.NewInt(1_000_000),
AmountOutMinimum: big.NewInt(950_000),
SqrtPriceLimitX96: big.NewInt(0),
}
innerCall, err := routerABI.Pack("exactInputSingle", params)
require.NoError(t, err)
multicallPayload, err := routerABI.Pack("multicall", big.NewInt(0), [][]byte{innerCall})
require.NoError(t, err)
rawSwap, err := decoder.DecodeSwapTransaction("uniswap_v3", multicallPayload)
require.NoError(t, err)
swap, ok := rawSwap.(*SwapEvent)
require.True(t, ok, "expected SwapEvent from multicall decode")
require.Equal(t, tokenIn, swap.TokenIn)
require.Equal(t, tokenOut, swap.TokenOut)
require.Equal(t, big.NewInt(1_000_000), swap.AmountIn)
require.Equal(t, big.NewInt(950_000), swap.AmountOut)
}

View File

@@ -0,0 +1,92 @@
package parser
import (
"context"
"fmt"
"time"
logpkg "github.com/fraktal/mev-beta/internal/logger"
pkgtypes "github.com/fraktal/mev-beta/pkg/types"
)
// OpportunityDispatcher represents the arbitrage service entry point that can
// accept opportunities discovered by the transaction analyzer.
type OpportunityDispatcher interface {
SubmitBridgeOpportunity(ctx context.Context, bridgeOpportunity interface{}) error
}
// Executor routes arbitrage opportunities discovered in the Arbitrum parser to
// the core arbitrage service.
type Executor struct {
logger *logpkg.Logger
dispatcher OpportunityDispatcher
metrics *ExecutorMetrics
serviceName string
}
// ExecutorMetrics captures lightweight counters about dispatched opportunities.
type ExecutorMetrics struct {
OpportunitiesForwarded int64
OpportunitiesRejected int64
LastDispatchTime time.Time
}
// NewExecutor creates a new parser executor that forwards opportunities to the
// provided dispatcher (typically the arbitrage service).
func NewExecutor(dispatcher OpportunityDispatcher, log *logpkg.Logger) *Executor {
if log == nil {
log = logpkg.New("info", "text", "")
}
return &Executor{
logger: log,
dispatcher: dispatcher,
metrics: &ExecutorMetrics{
OpportunitiesForwarded: 0,
OpportunitiesRejected: 0,
},
serviceName: "arbitrum-parser",
}
}
// ExecuteArbitrage forwards the opportunity to the arbitrage service.
func (e *Executor) ExecuteArbitrage(ctx context.Context, arbOp *pkgtypes.ArbitrageOpportunity) error {
if arbOp == nil {
e.metrics.OpportunitiesRejected++
return fmt.Errorf("arbitrage opportunity cannot be nil")
}
if e.dispatcher == nil {
e.metrics.OpportunitiesRejected++
return fmt.Errorf("no dispatcher configured for executor")
}
if ctx == nil {
ctx = context.Background()
}
e.logger.Info("Forwarding arbitrage opportunity detected by parser",
"id", arbOp.ID,
"path_length", len(arbOp.Path),
"pools", len(arbOp.Pools),
"profit", arbOp.NetProfit,
)
if err := e.dispatcher.SubmitBridgeOpportunity(ctx, arbOp); err != nil {
e.metrics.OpportunitiesRejected++
e.logger.Error("Failed to forward arbitrage opportunity",
"id", arbOp.ID,
"error", err,
)
return err
}
e.metrics.OpportunitiesForwarded++
e.metrics.LastDispatchTime = time.Now()
return nil
}
// Metrics returns a snapshot of executor metrics.
func (e *Executor) Metrics() ExecutorMetrics {
return *e.metrics
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
package parser
import (
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/fraktal/mev-beta/pkg/types"
)
// SwapParams represents parsed swap parameters
type SwapParams struct {
TokenIn common.Address
TokenOut common.Address
AmountIn *big.Int
AmountOut *big.Int
MinAmountOut *big.Int
Recipient common.Address
Fee *big.Int
Pool common.Address
}
// MEV opportunity structures
type MEVOpportunities struct {
BlockNumber uint64
Timestamp time.Time
SwapEvents []*SwapEvent
LiquidationEvents []*LiquidationEvent
LiquidityEvents []*LiquidityEvent
ArbitrageOps []*types.ArbitrageOpportunity
SandwichOps []*SandwichOpportunity
LiquidationOps []*LiquidationOpportunity
TotalProfit *big.Int
ProcessingTime time.Duration
}
// ParserOpportunity represents parser-specific arbitrage data (extends canonical ArbitrageOpportunity)
type ParserOpportunity struct {
*types.ArbitrageOpportunity
ProfitMargin float64
Exchanges []string
}
type SandwichOpportunity struct {
TargetTx string
TokenIn common.Address
TokenOut common.Address
FrontrunAmount *big.Int
BackrunAmount *big.Int
ExpectedProfit *big.Int
MaxSlippage float64
GasCost *big.Int
ProfitMargin float64
}
type LiquidationOpportunity struct {
Protocol string
Borrower common.Address
CollateralToken common.Address
DebtToken common.Address
MaxLiquidation *big.Int
ExpectedProfit *big.Int
HealthFactor float64
GasCost *big.Int
ProfitMargin float64
}
// RawL2Transaction represents a raw L2 transaction
type RawL2Transaction struct {
Hash string `json:"hash"`
From string `json:"from"`
To string `json:"to"`
Input string `json:"input"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Value string `json:"value"`
}
// RawL2Block represents a raw L2 block
type RawL2Block struct {
Number string `json:"number"`
Transactions []RawL2Transaction `json:"transactions"`
}
// SwapEvent represents a swap event
type SwapEvent struct {
Timestamp time.Time
BlockNumber uint64
TxHash string
Protocol string
Router common.Address
Pool common.Address
TokenIn common.Address
TokenOut common.Address
AmountIn *big.Int
AmountOut *big.Int
Sender common.Address
Recipient common.Address
GasPrice string
GasUsed uint64
PriceImpact float64
MEVScore float64
Profitable bool
// Additional fields for protocol-specific data
Fee uint64 // Uniswap V3 fee
Deadline uint64 // Transaction deadline
CurveI uint64 // Curve exchange input token index
CurveJ uint64 // Curve exchange output token index
}
// LiquidationEvent represents a liquidation event
type LiquidationEvent struct {
Timestamp time.Time
BlockNumber uint64
TxHash string
Protocol string
Liquidator common.Address
Borrower common.Address
CollateralToken common.Address
DebtToken common.Address
CollateralAmount *big.Int
DebtAmount *big.Int
Bonus *big.Int
HealthFactor float64
MEVOpportunity bool
EstimatedProfit *big.Int
}
// LiquidityEvent represents a liquidity event
type LiquidityEvent struct {
Timestamp time.Time
BlockNumber uint64
TxHash string
Protocol string
Pool common.Address
EventType string
Token0 common.Address
Token1 common.Address
Amount0 *big.Int
Amount1 *big.Int
Liquidity *big.Int
PriceAfter *big.Float
ImpactSize float64
ArbitrageOpp bool
}