feat(core): implement core MEV bot functionality with market scanning and Uniswap V3 pricing

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Krypto Kajun
2025-09-14 10:16:29 -05:00
parent 5db7587923
commit c16182d80c
1364 changed files with 473970 additions and 1202 deletions

View File

@@ -27,20 +27,20 @@ func NewArbitrumClient(endpoint string, logger *logger.Logger) (*ArbitrumClient,
if err != nil {
return nil, fmt.Errorf("failed to connect to Arbitrum RPC: %v", err)
}
ethClient := ethclient.NewClient(rpcClient)
// Get chain ID to verify we're connected to Arbitrum
chainID, err := ethClient.ChainID(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %v", err)
}
// Verify this is Arbitrum (42161 for mainnet, 421613 for testnet)
if chainID.Uint64() != 42161 && chainID.Uint64() != 421613 {
logger.Warn(fmt.Sprintf("Chain ID %d might not be Arbitrum mainnet (42161) or testnet (421613)", chainID.Uint64()))
}
return &ArbitrumClient{
Client: ethClient,
rpcClient: rpcClient,
@@ -55,18 +55,18 @@ func (c *ArbitrumClient) SubscribeToL2Messages(ctx context.Context, ch chan<- *L
if ctx == nil {
return nil, fmt.Errorf("context is nil")
}
if ch == nil {
return nil, fmt.Errorf("channel is nil")
}
// Subscribe to new heads to get L2 blocks
headers := make(chan *types.Header)
sub, err := c.SubscribeNewHead(ctx, headers)
if err != nil {
return nil, fmt.Errorf("failed to subscribe to new heads: %v", err)
}
// Process headers and extract L2 messages
go func() {
defer func() {
@@ -87,7 +87,7 @@ func (c *ArbitrumClient) SubscribeToL2Messages(ctx context.Context, ch chan<- *L
close(ch)
}
}()
for {
select {
case header := <-headers:
@@ -101,7 +101,7 @@ func (c *ArbitrumClient) SubscribeToL2Messages(ctx context.Context, ch chan<- *L
}
}
}()
return sub, nil
}
@@ -111,15 +111,15 @@ func (c *ArbitrumClient) processBlockForL2Messages(ctx context.Context, header *
if ctx == nil {
return fmt.Errorf("context is nil")
}
if header == nil {
return fmt.Errorf("header is nil")
}
if ch == nil {
return fmt.Errorf("channel is nil")
}
// For Arbitrum, we create L2 messages from the block data itself
// This represents the block as an L2 message containing potential transactions
l2Message := &L2Message{
@@ -130,7 +130,7 @@ func (c *ArbitrumClient) processBlockForL2Messages(ctx context.Context, header *
BlockNumber: header.Number.Uint64(),
BlockHash: header.Hash(),
}
// Try to get block transactions for more detailed analysis
block, err := c.BlockByHash(ctx, header.Hash())
if err != nil {
@@ -138,7 +138,7 @@ func (c *ArbitrumClient) processBlockForL2Messages(ctx context.Context, header *
} else if block != nil {
// Add transaction count and basic stats to the message
l2Message.TxCount = len(block.Transactions())
// For each transaction in the block, we could create separate L2 messages
// but to avoid overwhelming the system, we'll process them in batches
if len(block.Transactions()) > 0 {
@@ -146,13 +146,13 @@ func (c *ArbitrumClient) processBlockForL2Messages(ctx context.Context, header *
l2Message.Data = c.encodeTransactionsAsL2Message(block.Transactions())
}
}
select {
case ch <- l2Message:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
@@ -160,19 +160,19 @@ func (c *ArbitrumClient) processBlockForL2Messages(ctx context.Context, header *
func (c *ArbitrumClient) encodeBlockAsL2Message(header *types.Header) []byte {
// Create a simple encoding with block number and timestamp
data := make([]byte, 16) // 8 bytes for block number + 8 bytes for timestamp
// Encode block number (8 bytes)
blockNum := header.Number.Uint64()
for i := 0; i < 8; i++ {
data[i] = byte(blockNum >> (8 * (7 - i)))
}
// Encode timestamp (8 bytes)
timestamp := header.Time
for i := 0; i < 8; i++ {
data[8+i] = byte(timestamp >> (8 * (7 - i)))
}
return data
}
@@ -181,30 +181,30 @@ func (c *ArbitrumClient) encodeTransactionsAsL2Message(transactions []*types.Tra
if len(transactions) == 0 {
return []byte{}
}
// Create a simple encoding with transaction count and first few transaction hashes
data := make([]byte, 4) // Start with 4 bytes for transaction count
// Encode transaction count
txCount := uint32(len(transactions))
data[0] = byte(txCount >> 24)
data[1] = byte(txCount >> 16)
data[2] = byte(txCount >> 8)
data[3] = byte(txCount)
// Add up to first 3 transaction hashes (32 bytes each)
maxTxHashes := 3
if len(transactions) < maxTxHashes {
maxTxHashes = len(transactions)
}
for i := 0; i < maxTxHashes; i++ {
if transactions[i] != nil {
txHash := transactions[i].Hash()
data = append(data, txHash.Bytes()...)
}
}
return data
}
@@ -214,26 +214,26 @@ func (c *ArbitrumClient) extractL2MessageFromTransaction(tx *types.Transaction,
if len(tx.Data()) < 4 {
return nil
}
// Create L2 message
l2Message := &L2Message{
Type: L2Transaction,
Sender: common.Address{}, // Would need signature recovery
Data: tx.Data(),
Timestamp: timestamp,
TxHash: tx.Hash(),
GasUsed: tx.Gas(),
GasPrice: tx.GasPrice(),
ParsedTx: tx,
Type: L2Transaction,
Sender: common.Address{}, // Would need signature recovery
Data: tx.Data(),
Timestamp: timestamp,
TxHash: tx.Hash(),
GasUsed: tx.Gas(),
GasPrice: tx.GasPrice(),
ParsedTx: tx,
}
// Check if this is a DEX interaction for more detailed processing
if tx.To() != nil {
// We'll add more detailed DEX detection here
// For now, we mark all transactions as potential DEX interactions
// The parser will filter out non-DEX transactions
}
return l2Message
}
@@ -243,18 +243,18 @@ func (c *ArbitrumClient) GetL2TransactionReceipt(ctx context.Context, txHash com
if err != nil {
return nil, err
}
l2Receipt := &L2TransactionReceipt{
Receipt: receipt,
L2BlockNumber: receipt.BlockNumber.Uint64(),
L2TxIndex: uint64(receipt.TransactionIndex),
}
// Extract additional L2-specific data
if err := c.enrichL2Receipt(ctx, l2Receipt); err != nil {
c.Logger.Warn(fmt.Sprintf("Failed to enrich L2 receipt: %v", err))
}
return l2Receipt, nil
}
@@ -262,7 +262,7 @@ func (c *ArbitrumClient) GetL2TransactionReceipt(ctx context.Context, txHash com
func (c *ArbitrumClient) enrichL2Receipt(ctx context.Context, receipt *L2TransactionReceipt) error {
// This would use Arbitrum-specific RPC methods to get additional data
// For now, we'll add placeholder logic
// Check for retryable tickets in logs
for _, log := range receipt.Logs {
if c.isRetryableTicketLog(log) {
@@ -272,7 +272,7 @@ func (c *ArbitrumClient) enrichL2Receipt(ctx context.Context, receipt *L2Transac
}
}
}
return nil
}
@@ -288,19 +288,19 @@ func (c *ArbitrumClient) parseRetryableTicket(log *types.Log) (*RetryableTicket,
if len(log.Topics) < 3 {
return nil, fmt.Errorf("insufficient topics for retryable ticket")
}
ticket := &RetryableTicket{
TicketID: log.Topics[1],
From: common.BytesToAddress(log.Topics[2].Bytes()),
}
// Parse data field for additional parameters
if len(log.Data) >= 96 {
ticket.Value = new(big.Int).SetBytes(log.Data[:32])
ticket.MaxGas = new(big.Int).SetBytes(log.Data[32:64]).Uint64()
ticket.GasPriceBid = new(big.Int).SetBytes(log.Data[64:96])
}
return ticket, nil
}
@@ -312,25 +312,25 @@ func (c *ArbitrumClient) GetL2MessageByNumber(ctx context.Context, messageNumber
if err != nil {
return nil, fmt.Errorf("failed to get L2 message: %v", err)
}
// Parse the result into L2Message
l2Message := &L2Message{
MessageNumber: messageNumber,
Type: L2Unknown,
}
// Extract data from result map
if data, ok := result["data"].(string); ok {
l2Message.Data = common.FromHex(data)
}
if timestamp, ok := result["timestamp"].(string); ok {
ts := new(big.Int)
if _, success := ts.SetString(timestamp, 0); success {
l2Message.Timestamp = ts.Uint64()
}
}
return l2Message, nil
}
@@ -341,22 +341,22 @@ func (c *ArbitrumClient) GetBatchByNumber(ctx context.Context, batchNumber *big.
if err != nil {
return nil, fmt.Errorf("failed to get batch: %v", err)
}
batch := &BatchInfo{
BatchNumber: batchNumber,
}
if batchRoot, ok := result["batchRoot"].(string); ok {
batch.BatchRoot = common.HexToHash(batchRoot)
}
if txCount, ok := result["txCount"].(string); ok {
count := new(big.Int)
if _, success := count.SetString(txCount, 0); success {
batch.TxCount = count.Uint64()
}
}
return batch, nil
}
@@ -371,13 +371,13 @@ func (c *ArbitrumClient) SubscribeToNewBatches(ctx context.Context, ch chan<- *B
{common.HexToHash("0x8ca1a4adb985e8dd52c4b83e8e5ffa4ad1f6fca85ad893f4f9e5b45a5c1e5e9e")}, // SequencerBatchDelivered
},
}
logs := make(chan types.Log)
sub, err := c.SubscribeFilterLogs(ctx, query, logs)
if err != nil {
return nil, fmt.Errorf("failed to subscribe to batch logs: %v", err)
}
// Process logs and extract batch info
go func() {
defer close(ch)
@@ -396,7 +396,7 @@ func (c *ArbitrumClient) SubscribeToNewBatches(ctx context.Context, ch chan<- *B
}
}
}()
return sub, nil
}
@@ -405,19 +405,19 @@ func (c *ArbitrumClient) parseBatchFromLog(log types.Log) *BatchInfo {
if len(log.Topics) < 2 {
return nil
}
batchNumber := new(big.Int).SetBytes(log.Topics[1].Bytes())
batch := &BatchInfo{
BatchNumber: batchNumber,
L1SubmissionTx: log.TxHash,
}
if len(log.Data) >= 64 {
batch.BatchRoot = common.BytesToHash(log.Data[:32])
batch.TxCount = new(big.Int).SetBytes(log.Data[32:64]).Uint64()
}
return batch
}
@@ -425,4 +425,4 @@ func (c *ArbitrumClient) parseBatchFromLog(log types.Log) *BatchInfo {
func (c *ArbitrumClient) Close() {
c.Client.Close()
c.rpcClient.Close()
}
}

View File

@@ -15,34 +15,34 @@ import (
type L2GasEstimator struct {
client *ArbitrumClient
logger *logger.Logger
// L2 gas price configuration
baseFeeMultiplier float64
priorityFeeMin *big.Int
priorityFeeMax *big.Int
gasLimitMultiplier float64
baseFeeMultiplier float64
priorityFeeMin *big.Int
priorityFeeMax *big.Int
gasLimitMultiplier float64
}
// GasEstimate represents an L2 gas estimate with detailed breakdown
type GasEstimate struct {
GasLimit uint64
MaxFeePerGas *big.Int
MaxPriorityFee *big.Int
L1DataFee *big.Int
L2ComputeFee *big.Int
TotalFee *big.Int
Confidence float64 // 0-1 scale
GasLimit uint64
MaxFeePerGas *big.Int
MaxPriorityFee *big.Int
L1DataFee *big.Int
L2ComputeFee *big.Int
TotalFee *big.Int
Confidence float64 // 0-1 scale
}
// NewL2GasEstimator creates a new L2 gas estimator
func NewL2GasEstimator(client *ArbitrumClient, logger *logger.Logger) *L2GasEstimator {
return &L2GasEstimator{
client: client,
logger: logger,
baseFeeMultiplier: 1.1, // 10% buffer on base fee
priorityFeeMin: big.NewInt(100000000), // 0.1 gwei minimum
priorityFeeMax: big.NewInt(2000000000), // 2 gwei maximum
gasLimitMultiplier: 1.2, // 20% buffer on gas limit
client: client,
logger: logger,
baseFeeMultiplier: 1.1, // 10% buffer on base fee
priorityFeeMin: big.NewInt(100000000), // 0.1 gwei minimum
priorityFeeMax: big.NewInt(2000000000), // 2 gwei maximum
gasLimitMultiplier: 1.2, // 20% buffer on gas limit
}
}
@@ -53,35 +53,35 @@ func (g *L2GasEstimator) EstimateL2Gas(ctx context.Context, tx *types.Transactio
if err != nil {
return nil, fmt.Errorf("failed to get gas price: %v", err)
}
// Estimate gas limit
gasLimit, err := g.estimateGasLimit(ctx, tx)
if err != nil {
return nil, fmt.Errorf("failed to estimate gas limit: %v", err)
}
// Get L1 data fee (Arbitrum-specific)
l1DataFee, err := g.estimateL1DataFee(ctx, tx)
if err != nil {
g.logger.Warn(fmt.Sprintf("Failed to estimate L1 data fee: %v", err))
l1DataFee = big.NewInt(0)
}
// Calculate L2 compute fee
l2ComputeFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasLimit)))
// Calculate priority fee
priorityFee := g.calculateOptimalPriorityFee(ctx, gasPrice)
// Calculate max fee per gas
maxFeePerGas := new(big.Int).Add(gasPrice, priorityFee)
// Total fee includes both L1 and L2 components
totalFee := new(big.Int).Add(l1DataFee, l2ComputeFee)
// Apply gas limit buffer
bufferedGasLimit := uint64(float64(gasLimit) * g.gasLimitMultiplier)
estimate := &GasEstimate{
GasLimit: bufferedGasLimit,
MaxFeePerGas: maxFeePerGas,
@@ -91,7 +91,7 @@ func (g *L2GasEstimator) EstimateL2Gas(ctx context.Context, tx *types.Transactio
TotalFee: totalFee,
Confidence: g.calculateConfidence(gasPrice, priorityFee),
}
return estimate, nil
}
@@ -105,14 +105,14 @@ func (g *L2GasEstimator) estimateGasLimit(ctx context.Context, tx *types.Transac
Data: tx.Data(),
GasPrice: tx.GasPrice(),
}
// Estimate gas using the client
gasLimit, err := g.client.EstimateGas(ctx, msg)
if err != nil {
// Fallback to default gas limits based on transaction type
return g.getDefaultGasLimit(tx), nil
}
return gasLimit, nil
}
@@ -120,13 +120,13 @@ func (g *L2GasEstimator) estimateGasLimit(ctx context.Context, tx *types.Transac
func (g *L2GasEstimator) estimateL1DataFee(ctx context.Context, tx *types.Transaction) (*big.Int, error) {
// Arbitrum L1 data fee calculation
// This is based on the calldata size and L1 gas price
calldata := tx.Data()
// Count zero and non-zero bytes (different costs)
zeroBytes := 0
nonZeroBytes := 0
for _, b := range calldata {
if b == 0 {
zeroBytes++
@@ -134,19 +134,19 @@ func (g *L2GasEstimator) estimateL1DataFee(ctx context.Context, tx *types.Transa
nonZeroBytes++
}
}
// Arbitrum L1 data fee formula (simplified)
// Actual implementation would need to fetch current L1 gas price
l1GasPrice := big.NewInt(20000000000) // 20 gwei estimate
// Gas cost: 4 per zero byte, 16 per non-zero byte
gasCost := int64(zeroBytes*4 + nonZeroBytes*16)
// Add base transaction cost
gasCost += 21000
l1DataFee := new(big.Int).Mul(l1GasPrice, big.NewInt(gasCost))
return l1DataFee, nil
}
@@ -158,7 +158,7 @@ func (g *L2GasEstimator) calculateOptimalPriorityFee(ctx context.Context, baseFe
// Fallback to base fee percentage
priorityFee = new(big.Int).Div(baseFee, big.NewInt(10)) // 10% of base fee
}
// Ensure within bounds
if priorityFee.Cmp(g.priorityFeeMin) < 0 {
priorityFee = new(big.Int).Set(g.priorityFeeMin)
@@ -166,7 +166,7 @@ func (g *L2GasEstimator) calculateOptimalPriorityFee(ctx context.Context, baseFe
if priorityFee.Cmp(g.priorityFeeMax) > 0 {
priorityFee = new(big.Int).Set(g.priorityFeeMax)
}
return priorityFee
}
@@ -178,12 +178,12 @@ func (g *L2GasEstimator) getSuggestedPriorityFee(ctx context.Context) (*big.Int,
if err != nil {
return nil, err
}
priorityFee := new(big.Int)
if _, success := priorityFee.SetString(result[2:], 16); !success {
return nil, fmt.Errorf("invalid priority fee response")
}
return priorityFee, nil
}
@@ -192,7 +192,7 @@ func (g *L2GasEstimator) calculateConfidence(gasPrice, priorityFee *big.Int) flo
// Higher priority fee relative to gas price = higher confidence
ratio := new(big.Float).Quo(new(big.Float).SetInt(priorityFee), new(big.Float).SetInt(gasPrice))
ratioFloat, _ := ratio.Float64()
// Confidence scale: 0.1 ratio = 0.5 confidence, 0.5 ratio = 0.9 confidence
confidence := 0.3 + (ratioFloat * 1.2)
if confidence > 1.0 {
@@ -201,14 +201,14 @@ func (g *L2GasEstimator) calculateConfidence(gasPrice, priorityFee *big.Int) flo
if confidence < 0.1 {
confidence = 0.1
}
return confidence
}
// getDefaultGasLimit returns default gas limits based on transaction type
func (g *L2GasEstimator) getDefaultGasLimit(tx *types.Transaction) uint64 {
dataSize := len(tx.Data())
switch {
case dataSize == 0:
// Simple transfer
@@ -231,56 +231,56 @@ func (g *L2GasEstimator) getDefaultGasLimit(tx *types.Transaction) uint64 {
// OptimizeForSpeed adjusts gas parameters for fastest execution
func (g *L2GasEstimator) OptimizeForSpeed(estimate *GasEstimate) *GasEstimate {
optimized := *estimate
// Increase priority fee by 50%
speedPriorityFee := new(big.Int).Mul(estimate.MaxPriorityFee, big.NewInt(150))
optimized.MaxPriorityFee = new(big.Int).Div(speedPriorityFee, big.NewInt(100))
// Increase max fee per gas accordingly
optimized.MaxFeePerGas = new(big.Int).Add(
new(big.Int).Sub(estimate.MaxFeePerGas, estimate.MaxPriorityFee),
optimized.MaxPriorityFee,
)
// Increase gas limit by 10% more
optimized.GasLimit = uint64(float64(estimate.GasLimit) * 1.1)
// Recalculate total fee
l2Fee := new(big.Int).Mul(optimized.MaxFeePerGas, big.NewInt(int64(optimized.GasLimit)))
optimized.TotalFee = new(big.Int).Add(estimate.L1DataFee, l2Fee)
// Higher confidence due to aggressive pricing
optimized.Confidence = estimate.Confidence * 1.2
if optimized.Confidence > 1.0 {
optimized.Confidence = 1.0
}
return &optimized
}
// OptimizeForCost adjusts gas parameters for lowest cost
func (g *L2GasEstimator) OptimizeForCost(estimate *GasEstimate) *GasEstimate {
optimized := *estimate
// Use minimum priority fee
optimized.MaxPriorityFee = new(big.Int).Set(g.priorityFeeMin)
// Reduce max fee per gas
optimized.MaxFeePerGas = new(big.Int).Add(
new(big.Int).Sub(estimate.MaxFeePerGas, estimate.MaxPriorityFee),
optimized.MaxPriorityFee,
)
// Use exact gas limit (no buffer)
optimized.GasLimit = uint64(float64(estimate.GasLimit) / g.gasLimitMultiplier)
// Recalculate total fee
l2Fee := new(big.Int).Mul(optimized.MaxFeePerGas, big.NewInt(int64(optimized.GasLimit)))
optimized.TotalFee = new(big.Int).Add(estimate.L1DataFee, l2Fee)
// Lower confidence due to minimal gas pricing
optimized.Confidence = estimate.Confidence * 0.7
return &optimized
}
@@ -289,4 +289,3 @@ func (g *L2GasEstimator) IsL2TransactionViable(estimate *GasEstimate, expectedPr
// Compare total fee to expected profit
return estimate.TotalFee.Cmp(expectedProfit) < 0
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/fraktal/mev-beta/internal/logger"
"github.com/fraktal/mev-beta/pkg/pools"
)
// RawL2Transaction represents a raw Arbitrum L2 transaction
@@ -32,12 +33,34 @@ type RawL2Transaction struct {
// RawL2Block represents a raw Arbitrum L2 block
type RawL2Block struct {
Hash string `json:"hash"`
Number string `json:"number"`
Timestamp string `json:"timestamp"`
Hash string `json:"hash"`
Number string `json:"number"`
Timestamp string `json:"timestamp"`
Transactions []RawL2Transaction `json:"transactions"`
}
// RawL2BlockWithLogs represents a raw Arbitrum L2 block with logs
type RawL2BlockWithLogs struct {
Hash string `json:"hash"`
Number string `json:"number"`
Timestamp string `json:"timestamp"`
Transactions []RawL2TransactionWithLogs `json:"transactions"`
}
// RawL2TransactionWithLogs includes transaction logs for pool discovery
type RawL2TransactionWithLogs struct {
Hash string `json:"hash"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Gas string `json:"gas"`
GasPrice string `json:"gasPrice"`
Input string `json:"input"`
Logs []interface{} `json:"logs"`
TransactionIndex string `json:"transactionIndex"`
Type string `json:"type"`
}
// DEXFunctionSignature represents a DEX function signature
type DEXFunctionSignature struct {
Signature string
@@ -50,12 +73,15 @@ type DEXFunctionSignature struct {
type ArbitrumL2Parser struct {
client *rpc.Client
logger *logger.Logger
// DEX contract addresses on Arbitrum
dexContracts map[common.Address]string
// DEX function signatures
dexFunctions map[string]DEXFunctionSignature
// Pool discovery system
poolDiscovery *pools.PoolDiscovery
}
// NewArbitrumL2Parser creates a new Arbitrum L2 transaction parser
@@ -66,8 +92,8 @@ func NewArbitrumL2Parser(rpcEndpoint string, logger *logger.Logger) (*ArbitrumL2
}
parser := &ArbitrumL2Parser{
client: client,
logger: logger,
client: client,
logger: logger,
dexContracts: make(map[common.Address]string),
dexFunctions: make(map[string]DEXFunctionSignature),
}
@@ -75,6 +101,11 @@ func NewArbitrumL2Parser(rpcEndpoint string, logger *logger.Logger) (*ArbitrumL2
// Initialize DEX contracts and functions
parser.initializeDEXData()
// Initialize pool discovery system
parser.poolDiscovery = pools.NewPoolDiscovery(client, logger)
logger.Info(fmt.Sprintf("Pool discovery system initialized - %d pools, %d exchanges loaded",
parser.poolDiscovery.GetPoolCount(), parser.poolDiscovery.GetExchangeCount()))
return parser, nil
}
@@ -91,7 +122,7 @@ func (p *ArbitrumL2Parser) initializeDEXData() {
p.dexContracts[common.HexToAddress("0xC36442b4a4522E871399CD717aBDD847Ab11FE88")] = "UniswapV3PositionManager"
// CORRECT DEX function signatures verified for Arbitrum (first 4 bytes of keccak256(function_signature))
// Uniswap V2 swap functions
p.dexFunctions["0x38ed1739"] = DEXFunctionSignature{
Signature: "0x38ed1739",
@@ -141,7 +172,7 @@ func (p *ArbitrumL2Parser) initializeDEXData() {
Protocol: "UniswapV2",
Description: "Swap exact tokens for tokens supporting fee-on-transfer tokens",
}
// Uniswap V2 liquidity functions
p.dexFunctions["0xe8e33700"] = DEXFunctionSignature{
Signature: "0xe8e33700",
@@ -167,7 +198,7 @@ func (p *ArbitrumL2Parser) initializeDEXData() {
Protocol: "UniswapV2",
Description: "Remove liquidity with ETH",
}
// Uniswap V3 swap functions
p.dexFunctions["0x414bf389"] = DEXFunctionSignature{
Signature: "0x414bf389",
@@ -199,7 +230,7 @@ func (p *ArbitrumL2Parser) initializeDEXData() {
Protocol: "UniswapV3",
Description: "Batch multiple function calls",
}
// Uniswap V3 position management functions
p.dexFunctions["0x88316456"] = DEXFunctionSignature{
Signature: "0x88316456",
@@ -230,7 +261,7 @@ func (p *ArbitrumL2Parser) initializeDEXData() {
// GetBlockByNumber fetches a block with full transaction details using raw RPC
func (p *ArbitrumL2Parser) GetBlockByNumber(ctx context.Context, blockNumber uint64) (*RawL2Block, error) {
var block RawL2Block
blockNumHex := fmt.Sprintf("0x%x", blockNumber)
err := p.client.CallContext(ctx, &block, "eth_getBlockByNumber", blockNumHex, true)
if err != nil {
@@ -260,30 +291,30 @@ func (p *ArbitrumL2Parser) ParseDEXTransactions(ctx context.Context, block *RawL
// SwapDetails contains detailed information about a DEX swap
type SwapDetails struct {
AmountIn *big.Int
AmountOut *big.Int
AmountMin *big.Int
TokenIn string
TokenOut string
Fee uint32
Deadline uint64
Recipient string
IsValid bool
AmountIn *big.Int
AmountOut *big.Int
AmountMin *big.Int
TokenIn string
TokenOut string
Fee uint32
Deadline uint64
Recipient string
IsValid bool
}
// DEXTransaction represents a parsed DEX transaction
type DEXTransaction struct {
Hash string
From string
To string
Value *big.Int
FunctionSig string
FunctionName string
Protocol string
InputData []byte
ContractName string
BlockNumber string
SwapDetails *SwapDetails // Detailed swap information
Hash string
From string
To string
Value *big.Int
FunctionSig string
FunctionName string
Protocol string
InputData []byte
ContractName string
BlockNumber string
SwapDetails *SwapDetails // Detailed swap information
}
// parseDEXTransaction checks if a transaction is a DEX interaction
@@ -299,16 +330,16 @@ func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransact
}
toAddr := common.HexToAddress(tx.To)
// Check if transaction is to a known DEX contract
contractName, isDEXContract := p.dexContracts[toAddr]
// Extract function signature (first 4 bytes of input data)
functionSig := tx.Input[:10] // "0x" + 8 hex chars = 10 chars
// Check if function signature matches known DEX functions
if funcInfo, isDEXFunction := p.dexFunctions[functionSig]; isDEXFunction {
// Parse value
value := big.NewInt(0)
if tx.Value != "" && tx.Value != "0x" && tx.Value != "0x0" {
@@ -324,7 +355,7 @@ func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransact
// Decode function parameters based on function type
swapDetails := p.decodeFunctionDataStructured(funcInfo, inputData)
// Use detailed opportunity logging if swap details are available
if swapDetails != nil && swapDetails.IsValid && swapDetails.AmountIn != nil {
amountInFloat := new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountIn), big.NewFloat(1e18))
@@ -337,27 +368,27 @@ func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransact
amountMinFloat, _ = new(big.Float).Quo(new(big.Float).SetInt(swapDetails.AmountMin), big.NewFloat(1e18)).Float64()
}
amountInFloatVal, _ := amountInFloat.Float64()
// Calculate estimated profit (placeholder - would need price oracle in real implementation)
estimatedProfitUSD := 0.0
additionalData := map[string]interface{}{
"tokenIn": swapDetails.TokenIn,
"tokenOut": swapDetails.TokenOut,
"fee": swapDetails.Fee,
"deadline": swapDetails.Deadline,
"recipient": swapDetails.Recipient,
"tokenIn": swapDetails.TokenIn,
"tokenOut": swapDetails.TokenOut,
"fee": swapDetails.Fee,
"deadline": swapDetails.Deadline,
"recipient": swapDetails.Recipient,
"contractName": contractName,
"functionSig": functionSig,
"functionSig": functionSig,
}
p.logger.Opportunity(tx.Hash, tx.From, tx.To, funcInfo.Name, funcInfo.Protocol,
p.logger.Opportunity(tx.Hash, tx.From, tx.To, funcInfo.Name, funcInfo.Protocol,
amountInFloatVal, amountOutFloat, amountMinFloat, estimatedProfitUSD, additionalData)
} else {
// Fallback to simple logging
swapDetailsStr := p.decodeFunctionData(funcInfo, inputData)
p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s), Value: %s ETH%s",
tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol,
p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s), Value: %s ETH%s",
tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol,
new(big.Float).Quo(new(big.Float).SetInt(value), big.NewFloat(1e18)).String(), swapDetailsStr))
}
@@ -378,7 +409,7 @@ func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransact
// Check if it's to a known DEX contract but unknown function
if isDEXContract {
p.logger.Debug(fmt.Sprintf("Unknown DEX function call: %s -> %s (%s), Function: %s",
p.logger.Debug(fmt.Sprintf("Unknown DEX function call: %s -> %s (%s), Function: %s",
tx.From, tx.To, contractName, functionSig))
}
@@ -390,10 +421,10 @@ func (p *ArbitrumL2Parser) decodeFunctionData(funcInfo DEXFunctionSignature, inp
if len(inputData) < 4 {
return ""
}
// Skip the 4-byte function selector
params := inputData[4:]
switch funcInfo.Name {
case "swapExactTokensForTokens":
return p.decodeSwapExactTokensForTokens(params)
@@ -422,32 +453,32 @@ func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokens(params []byte) string
if len(params) < 160 { // 5 parameters * 32 bytes each
return ", Invalid parameters"
}
// Decode parameters (simplified - real ABI decoding would be more robust)
amountIn := new(big.Int).SetBytes(params[0:32])
amountOutMin := new(big.Int).SetBytes(params[32:64])
// Convert to readable format
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens, MinOut: %s tokens",
return fmt.Sprintf(", AmountIn: %s tokens, MinOut: %s tokens",
amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6))
}
// decodeSwapTokensForExactTokens decodes UniswapV2 swapTokensForExactTokens parameters
// decodeSwapTokensForExactTokens decodes UniswapV2 swapTokensForExactTokens parameters
func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokens(params []byte) string {
if len(params) < 160 {
return ", Invalid parameters"
}
amountOut := new(big.Int).SetBytes(params[0:32])
amountInMax := new(big.Int).SetBytes(params[32:64])
amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18))
amountInMaxEth := new(big.Float).Quo(new(big.Float).SetInt(amountInMax), big.NewFloat(1e18))
return fmt.Sprintf(", AmountOut: %s tokens, MaxIn: %s tokens",
return fmt.Sprintf(", AmountOut: %s tokens, MaxIn: %s tokens",
amountOutEth.Text('f', 6), amountInMaxEth.Text('f', 6))
}
@@ -456,10 +487,10 @@ func (p *ArbitrumL2Parser) decodeSwapExactETHForTokens(params []byte) string {
if len(params) < 32 {
return ", Invalid parameters"
}
amountOutMin := new(big.Int).SetBytes(params[0:32])
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
return fmt.Sprintf(", MinOut: %s tokens", amountOutMinEth.Text('f', 6))
}
@@ -468,14 +499,14 @@ func (p *ArbitrumL2Parser) decodeSwapExactTokensForETH(params []byte) string {
if len(params) < 64 {
return ", Invalid parameters"
}
amountIn := new(big.Int).SetBytes(params[0:32])
amountOutMin := new(big.Int).SetBytes(params[32:64])
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
amountOutMinEth := new(big.Float).Quo(new(big.Float).SetInt(amountOutMin), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens, MinETH: %s",
return fmt.Sprintf(", AmountIn: %s tokens, MinETH: %s",
amountInEth.Text('f', 6), amountOutMinEth.Text('f', 6))
}
@@ -484,11 +515,11 @@ func (p *ArbitrumL2Parser) decodeExactInputSingle(params []byte) string {
if len(params) < 160 { // ExactInputSingleParams struct
return ", Invalid parameters"
}
// Simplified decoding - real implementation would parse the struct properly
amountIn := new(big.Int).SetBytes(params[128:160]) // approximation
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens", amountInEth.Text('f', 6))
}
@@ -497,10 +528,10 @@ func (p *ArbitrumL2Parser) decodeExactInput(params []byte) string {
if len(params) < 128 {
return ", Invalid parameters"
}
amountIn := new(big.Int).SetBytes(params[64:96]) // approximation
amountInEth := new(big.Float).Quo(new(big.Float).SetInt(amountIn), big.NewFloat(1e18))
return fmt.Sprintf(", AmountIn: %s tokens (multi-hop)", amountInEth.Text('f', 6))
}
@@ -509,10 +540,10 @@ func (p *ArbitrumL2Parser) decodeExactOutputSingle(params []byte) string {
if len(params) < 160 {
return ", Invalid parameters"
}
amountOut := new(big.Int).SetBytes(params[160:192]) // approximation
amountOutEth := new(big.Float).Quo(new(big.Float).SetInt(amountOut), big.NewFloat(1e18))
return fmt.Sprintf(", AmountOut: %s tokens", amountOutEth.Text('f', 6))
}
@@ -521,7 +552,7 @@ func (p *ArbitrumL2Parser) decodeMulticall(params []byte) string {
if len(params) < 32 {
return ", Invalid parameters"
}
// Multicall contains an array of encoded function calls
// This is complex to decode without full ABI parsing
return fmt.Sprintf(", Multicall with %d bytes of data", len(params))
@@ -532,10 +563,10 @@ func (p *ArbitrumL2Parser) decodeFunctionDataStructured(funcInfo DEXFunctionSign
if len(inputData) < 4 {
return &SwapDetails{IsValid: false}
}
// Skip the 4-byte function selector
params := inputData[4:]
switch funcInfo.Name {
case "swapExactTokensForTokens":
return p.decodeSwapExactTokensForTokensStructured(params)
@@ -563,15 +594,15 @@ func (p *ArbitrumL2Parser) decodeSwapExactTokensForTokensStructured(params []byt
if len(params) < 160 { // 5 parameters * 32 bytes each
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountIn: new(big.Int).SetBytes(params[0:32]),
AmountMin: new(big.Int).SetBytes(params[32:64]),
TokenIn: "unknown", // Would need to decode path array
TokenOut: "unknown", // Would need to decode path array
Deadline: new(big.Int).SetBytes(params[128:160]).Uint64(),
Recipient: fmt.Sprintf("0x%x", params[96:128]), // address is last 20 bytes
IsValid: true,
AmountIn: new(big.Int).SetBytes(params[0:32]),
AmountMin: new(big.Int).SetBytes(params[32:64]),
TokenIn: "unknown", // Would need to decode path array
TokenOut: "unknown", // Would need to decode path array
Deadline: new(big.Int).SetBytes(params[128:160]).Uint64(),
Recipient: fmt.Sprintf("0x%x", params[96:128]), // address is last 20 bytes
IsValid: true,
}
}
@@ -580,13 +611,13 @@ func (p *ArbitrumL2Parser) decodeSwapExactTokensForETHStructured(params []byte)
if len(params) < 64 {
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountIn: new(big.Int).SetBytes(params[0:32]),
AmountMin: new(big.Int).SetBytes(params[32:64]),
TokenIn: "unknown",
TokenOut: "ETH",
IsValid: true,
AmountIn: new(big.Int).SetBytes(params[0:32]),
AmountMin: new(big.Int).SetBytes(params[32:64]),
TokenIn: "unknown",
TokenOut: "ETH",
IsValid: true,
}
}
@@ -595,15 +626,15 @@ func (p *ArbitrumL2Parser) decodeExactInputSingleStructured(params []byte) *Swap
if len(params) < 160 { // ExactInputSingleParams struct
return &SwapDetails{IsValid: false}
}
// Simplified decoding - real implementation would parse the struct properly
return &SwapDetails{
AmountIn: new(big.Int).SetBytes(params[128:160]),
TokenIn: fmt.Sprintf("0x%x", params[0:32]), // tokenIn
TokenOut: fmt.Sprintf("0x%x", params[32:64]), // tokenOut
Fee: uint32(new(big.Int).SetBytes(params[64:96]).Uint64()), // fee
Recipient: fmt.Sprintf("0x%x", params[96:128]), // recipient
IsValid: true,
AmountIn: new(big.Int).SetBytes(params[128:160]),
TokenIn: fmt.Sprintf("0x%x", params[0:32]), // tokenIn
TokenOut: fmt.Sprintf("0x%x", params[32:64]), // tokenOut
Fee: uint32(new(big.Int).SetBytes(params[64:96]).Uint64()), // fee
Recipient: fmt.Sprintf("0x%x", params[96:128]), // recipient
IsValid: true,
}
}
@@ -612,13 +643,13 @@ func (p *ArbitrumL2Parser) decodeSwapTokensForExactTokensStructured(params []byt
if len(params) < 160 {
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountOut: new(big.Int).SetBytes(params[0:32]),
AmountIn: new(big.Int).SetBytes(params[32:64]), // Max amount in
TokenIn: "unknown",
TokenOut: "unknown",
IsValid: true,
AmountOut: new(big.Int).SetBytes(params[0:32]),
AmountIn: new(big.Int).SetBytes(params[32:64]), // Max amount in
TokenIn: "unknown",
TokenOut: "unknown",
IsValid: true,
}
}
@@ -627,12 +658,12 @@ func (p *ArbitrumL2Parser) decodeSwapExactETHForTokensStructured(params []byte)
if len(params) < 32 {
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountMin: new(big.Int).SetBytes(params[0:32]),
TokenIn: "ETH",
TokenOut: "unknown",
IsValid: true,
AmountMin: new(big.Int).SetBytes(params[0:32]),
TokenIn: "ETH",
TokenOut: "unknown",
IsValid: true,
}
}
@@ -641,12 +672,12 @@ func (p *ArbitrumL2Parser) decodeExactInputStructured(params []byte) *SwapDetail
if len(params) < 128 {
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountIn: new(big.Int).SetBytes(params[64:96]),
TokenIn: "unknown", // Would need to decode path
TokenOut: "unknown", // Would need to decode path
IsValid: true,
AmountIn: new(big.Int).SetBytes(params[64:96]),
TokenIn: "unknown", // Would need to decode path
TokenOut: "unknown", // Would need to decode path
IsValid: true,
}
}
@@ -655,12 +686,12 @@ func (p *ArbitrumL2Parser) decodeExactOutputSingleStructured(params []byte) *Swa
if len(params) < 160 {
return &SwapDetails{IsValid: false}
}
return &SwapDetails{
AmountOut: new(big.Int).SetBytes(params[160:192]),
TokenIn: fmt.Sprintf("0x%x", params[0:32]),
TokenOut: fmt.Sprintf("0x%x", params[32:64]),
IsValid: true,
AmountOut: new(big.Int).SetBytes(params[160:192]),
TokenIn: fmt.Sprintf("0x%x", params[0:32]),
TokenOut: fmt.Sprintf("0x%x", params[32:64]),
IsValid: true,
}
}
@@ -669,13 +700,13 @@ func (p *ArbitrumL2Parser) decodeMulticallStructured(params []byte) *SwapDetails
if len(params) < 32 {
return &SwapDetails{IsValid: false}
}
// For multicall, we'd need to decode the individual calls
// This is a placeholder
return &SwapDetails{
TokenIn: "unknown",
TokenOut: "unknown",
IsValid: true,
TokenIn: "unknown",
TokenOut: "unknown",
IsValid: true,
}
}
@@ -684,4 +715,4 @@ func (p *ArbitrumL2Parser) Close() {
if p.client != nil {
p.client.Close()
}
}
}

View File

@@ -16,13 +16,13 @@ import (
// L2MessageParser parses Arbitrum L2 messages and transactions
type L2MessageParser struct {
logger *logger.Logger
logger *logger.Logger
uniswapV2RouterABI abi.ABI
uniswapV3RouterABI abi.ABI
// Known DEX contract addresses on Arbitrum
knownRouters map[common.Address]string
knownPools map[common.Address]string
knownRouters map[common.Address]string
knownPools map[common.Address]string
}
// NewL2MessageParser creates a new L2 message parser
@@ -32,13 +32,13 @@ func NewL2MessageParser(logger *logger.Logger) *L2MessageParser {
knownRouters: make(map[common.Address]string),
knownPools: make(map[common.Address]string),
}
// Initialize known Arbitrum DEX addresses
parser.initializeKnownAddresses()
// Load ABIs for parsing
parser.loadABIs()
return parser
}
@@ -47,25 +47,25 @@ func (p *L2MessageParser) initializeKnownAddresses() {
// Uniswap V3 on Arbitrum
p.knownRouters[common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")] = "UniswapV3"
p.knownRouters[common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")] = "UniswapV3Router2"
// Uniswap V2 on Arbitrum
p.knownRouters[common.HexToAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")] = "UniswapV2"
// SushiSwap on Arbitrum
p.knownRouters[common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506")] = "SushiSwap"
// Camelot DEX (Arbitrum native)
p.knownRouters[common.HexToAddress("0xc873fEcbd354f5A56E00E710B90EF4201db2448d")] = "Camelot"
// GMX
p.knownRouters[common.HexToAddress("0x327df1e6de05895d2ab08513aadd9317845f20d9")] = "GMX"
// Balancer V2
p.knownRouters[common.HexToAddress("0xBA12222222228d8Ba445958a75a0704d566BF2C8")] = "BalancerV2"
// Curve
p.knownRouters[common.HexToAddress("0x98EE8517825C0bd778a57471a27555614F97F48D")] = "Curve"
// Popular pools on Arbitrum
p.knownPools[common.HexToAddress("0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443")] = "ETH/USDC-0.05%"
p.knownPools[common.HexToAddress("0x17c14D2c404D167802b16C450d3c99F88F2c4F4d")] = "ETH/USDC-0.3%"
@@ -91,7 +91,7 @@ func (p *L2MessageParser) loadABIs() {
"type": "function"
}
]`
var err error
p.uniswapV2RouterABI, err = abi.JSON(bytes.NewReader([]byte(uniswapV2RouterABI)))
if err != nil {
@@ -105,39 +105,39 @@ func (p *L2MessageParser) ParseL2Message(messageData []byte, messageNumber *big.
if messageData == nil {
return nil, fmt.Errorf("message data is nil")
}
if len(messageData) < 4 {
return nil, fmt.Errorf("message data too short: %d bytes", len(messageData))
}
// Validate message number
if messageNumber == nil {
return nil, fmt.Errorf("message number is nil")
}
// Validate timestamp (should be a reasonable Unix timestamp)
if timestamp > uint64(time.Now().Unix()+86400) || timestamp < 1609459200 { // 1609459200 = 2021-01-01
p.logger.Warn(fmt.Sprintf("Suspicious timestamp: %d", timestamp))
// We'll still process it but log the warning
}
l2Message := &L2Message{
MessageNumber: messageNumber,
Data: messageData,
Timestamp: timestamp,
Type: L2Unknown,
}
// Parse message type from first bytes
msgType := binary.BigEndian.Uint32(messageData[:4])
// Validate message type
if msgType != 3 && msgType != 7 {
p.logger.Debug(fmt.Sprintf("Unknown L2 message type: %d", msgType))
// We'll still return the message but mark it as unknown
return l2Message, nil
}
switch msgType {
case 3: // L2 Transaction
return p.parseL2Transaction(l2Message, messageData[4:])
@@ -155,42 +155,42 @@ func (p *L2MessageParser) parseL2Transaction(l2Message *L2Message, data []byte)
if l2Message == nil {
return nil, fmt.Errorf("l2Message is nil")
}
if data == nil {
return nil, fmt.Errorf("transaction data is nil")
}
// Validate data length
if len(data) == 0 {
return nil, fmt.Errorf("transaction data is empty")
}
l2Message.Type = L2Transaction
// Parse RLP-encoded transaction
tx := &types.Transaction{}
if err := tx.UnmarshalBinary(data); err != nil {
return nil, fmt.Errorf("failed to unmarshal transaction: %v", err)
}
// Validate the parsed transaction
if tx == nil {
return nil, fmt.Errorf("parsed transaction is nil")
}
// Additional validation for transaction fields
if tx.Gas() == 0 && len(tx.Data()) == 0 {
p.logger.Warn("Transaction has zero gas and no data")
}
l2Message.ParsedTx = tx
// Extract sender (this might require signature recovery)
if tx.To() != nil {
// For now, we'll extract what we can without signature recovery
l2Message.Sender = common.HexToAddress("0x0") // Placeholder
}
return l2Message, nil
}
@@ -200,40 +200,40 @@ func (p *L2MessageParser) parseL2Batch(l2Message *L2Message, data []byte) (*L2Me
if l2Message == nil {
return nil, fmt.Errorf("l2Message is nil")
}
if data == nil {
return nil, fmt.Errorf("batch data is nil")
}
l2Message.Type = L2BatchSubmission
// Parse batch data structure
if len(data) < 32 {
return nil, fmt.Errorf("batch data too short: %d bytes", len(data))
}
// Extract batch index
batchIndex := new(big.Int).SetBytes(data[:32])
// Validate batch index
if batchIndex == nil || batchIndex.Sign() < 0 {
return nil, fmt.Errorf("invalid batch index")
}
l2Message.BatchIndex = batchIndex
// Parse individual transactions in the batch
remainingData := data[32:]
// Validate remaining data
if remainingData == nil {
// No transactions in the batch, which is valid
l2Message.InnerTxs = []*types.Transaction{}
return l2Message, nil
}
var innerTxs []*types.Transaction
for len(remainingData) > 0 {
// Each transaction is prefixed with its length
if len(remainingData) < 4 {
@@ -241,25 +241,25 @@ func (p *L2MessageParser) parseL2Batch(l2Message *L2Message, data []byte) (*L2Me
p.logger.Warn("Incomplete transaction length prefix in batch")
break
}
txLength := binary.BigEndian.Uint32(remainingData[:4])
// Validate transaction length
if txLength == 0 {
p.logger.Warn("Zero-length transaction in batch")
remainingData = remainingData[4:]
continue
}
if uint32(len(remainingData)) < 4+txLength {
// Incomplete transaction data, log warning but continue with what we have
p.logger.Warn(fmt.Sprintf("Incomplete transaction data in batch: expected %d bytes, got %d", txLength, len(remainingData)-4))
break
}
txData := remainingData[4 : 4+txLength]
tx := &types.Transaction{}
if err := tx.UnmarshalBinary(txData); err == nil {
// Validate the parsed transaction
if tx != nil {
@@ -271,10 +271,10 @@ func (p *L2MessageParser) parseL2Batch(l2Message *L2Message, data []byte) (*L2Me
// Log the error but continue processing other transactions
p.logger.Warn(fmt.Sprintf("Failed to unmarshal transaction in batch: %v", err))
}
remainingData = remainingData[4+txLength:]
}
l2Message.InnerTxs = innerTxs
return l2Message, nil
}
@@ -285,18 +285,18 @@ func (p *L2MessageParser) ParseDEXInteraction(tx *types.Transaction) (*DEXIntera
if tx == nil {
return nil, fmt.Errorf("transaction is nil")
}
if tx.To() == nil {
return nil, fmt.Errorf("contract creation transaction")
}
to := *tx.To()
// Validate address
if to == (common.Address{}) {
return nil, fmt.Errorf("invalid contract address")
}
protocol, isDEX := p.knownRouters[to]
if !isDEX {
// Also check if this might be a direct pool interaction
@@ -306,31 +306,31 @@ func (p *L2MessageParser) ParseDEXInteraction(tx *types.Transaction) (*DEXIntera
return nil, fmt.Errorf("not a known DEX router or pool")
}
}
data := tx.Data()
// Validate transaction data
if data == nil {
return nil, fmt.Errorf("transaction data is nil")
}
if len(data) < 4 {
return nil, fmt.Errorf("transaction data too short: %d bytes", len(data))
}
// Validate function selector (first 4 bytes)
selector := data[:4]
if len(selector) != 4 {
return nil, fmt.Errorf("invalid function selector length: %d", len(selector))
}
interaction := &DEXInteraction{
Protocol: protocol,
Router: to,
Timestamp: uint64(time.Now().Unix()), // Use current time as default
MessageNumber: big.NewInt(0), // Will be set by caller
Protocol: protocol,
Router: to,
Timestamp: uint64(time.Now().Unix()), // Use current time as default
MessageNumber: big.NewInt(0), // Will be set by caller
}
// Parse based on function selector
switch common.Bytes2Hex(selector) {
case "38ed1739": // swapExactTokensForTokens (Uniswap V2)
@@ -368,78 +368,78 @@ func (p *L2MessageParser) parseSwapExactTokensForTokens(interaction *DEXInteract
if interaction == nil {
return nil, fmt.Errorf("interaction is nil")
}
if data == nil {
return nil, fmt.Errorf("data is nil")
}
// Decode ABI data
method, err := p.uniswapV2RouterABI.MethodById(crypto.Keccak256([]byte("swapExactTokensForTokens(uint256,uint256,address[],address,uint256)"))[:4])
if err != nil {
return nil, fmt.Errorf("failed to get ABI method: %v", err)
}
// Validate data length before unpacking
if len(data) == 0 {
return nil, fmt.Errorf("data is empty")
}
inputs, err := method.Inputs.Unpack(data)
if err != nil {
return nil, fmt.Errorf("failed to unpack ABI data: %v", err)
}
if len(inputs) < 5 {
return nil, fmt.Errorf("insufficient swap parameters: got %d, expected 5", len(inputs))
}
// Extract parameters with validation
amountIn, ok := inputs[0].(*big.Int)
if !ok {
return nil, fmt.Errorf("amountIn is not a *big.Int")
}
// Validate amountIn is not negative
if amountIn.Sign() < 0 {
return nil, fmt.Errorf("negative amountIn")
}
interaction.AmountIn = amountIn
// amountOutMin := inputs[1].(*big.Int)
path, ok := inputs[2].([]common.Address)
if !ok {
return nil, fmt.Errorf("path is not []common.Address")
}
// Validate path
if len(path) < 2 {
return nil, fmt.Errorf("path must contain at least 2 tokens, got %d", len(path))
}
// Validate addresses in path are not zero
for i, addr := range path {
if addr == (common.Address{}) {
return nil, fmt.Errorf("zero address in path at index %d", i)
}
}
recipient, ok := inputs[3].(common.Address)
if !ok {
return nil, fmt.Errorf("recipient is not common.Address")
}
// Validate recipient is not zero
if recipient == (common.Address{}) {
return nil, fmt.Errorf("recipient address is zero")
}
interaction.Recipient = recipient
interaction.Deadline = inputs[4].(*big.Int).Uint64()
interaction.TokenIn = path[0]
interaction.TokenOut = path[len(path)-1]
return interaction, nil
}
@@ -487,11 +487,11 @@ func (p *L2MessageParser) parseExactInputSingle(interaction *DEXInteraction, dat
if interaction == nil {
return nil, fmt.Errorf("interaction is nil")
}
if data == nil {
return nil, fmt.Errorf("data is nil")
}
// Uniswap V3 exactInputSingle structure:
// struct ExactInputSingleParams {
// address tokenIn;
@@ -503,33 +503,33 @@ func (p *L2MessageParser) parseExactInputSingle(interaction *DEXInteraction, dat
// uint256 amountOutMinimum;
// uint160 sqrtPriceLimitX96;
// }
// Validate minimum data length (at least 8 parameters * 32 bytes each)
if len(data) < 256 {
return nil, fmt.Errorf("insufficient data for exactInputSingle: %d bytes", len(data))
}
// Parse parameters with bounds checking
// tokenIn (first parameter) - bytes 0-31, address is in last 20 bytes (12-31)
if len(data) >= 32 {
interaction.TokenIn = common.BytesToAddress(data[12:32])
}
// tokenOut (second parameter) - bytes 32-63, address is in last 20 bytes (44-63)
if len(data) >= 64 {
interaction.TokenOut = common.BytesToAddress(data[44:64])
}
// recipient (fourth parameter) - bytes 96-127, address is in last 20 bytes (108-127)
if len(data) >= 128 {
interaction.Recipient = common.BytesToAddress(data[108:128])
}
// deadline (fifth parameter) - bytes 128-159, uint64 is in last 8 bytes (152-159)
if len(data) >= 160 {
interaction.Deadline = binary.BigEndian.Uint64(data[152:160])
}
// amountIn (sixth parameter) - bytes 160-191
if len(data) >= 192 {
amountIn := new(big.Int).SetBytes(data[160:192])
@@ -539,22 +539,22 @@ func (p *L2MessageParser) parseExactInputSingle(interaction *DEXInteraction, dat
}
interaction.AmountIn = amountIn
}
// Set default values for fields that might not be parsed
if interaction.AmountOut == nil {
interaction.AmountOut = big.NewInt(0)
}
// Validate that we have required fields
if interaction.TokenIn == (common.Address{}) && interaction.TokenOut == (common.Address{}) {
// If both are zero, we likely don't have valid data
return nil, fmt.Errorf("unable to parse token addresses from data")
}
// Note: We're not strictly validating that addresses are non-zero since some
// transactions might legitimately use zero addresses in certain contexts
// The calling code should validate addresses as appropriate for their use case
return interaction, nil
}
@@ -571,35 +571,35 @@ func (p *L2MessageParser) IsSignificantSwap(interaction *DEXInteraction, minAmou
p.logger.Warn("IsSignificantSwap called with nil interaction")
return false
}
// Validate minAmountUSD
if minAmountUSD < 0 {
p.logger.Warn(fmt.Sprintf("Negative minAmountUSD: %f", minAmountUSD))
return false
}
// This would implement logic to determine if the swap is large enough
// to be worth monitoring for arbitrage opportunities
// For now, check if amount is above a threshold
if interaction.AmountIn == nil {
return false
}
// Validate AmountIn is not negative
if interaction.AmountIn.Sign() < 0 {
p.logger.Warn("Negative AmountIn in DEX interaction")
return false
}
// Simplified check - in practice, you'd convert to USD value
threshold := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) // 1 ETH worth
// Validate threshold
if threshold == nil || threshold.Sign() <= 0 {
p.logger.Error("Invalid threshold calculation")
return false
}
return interaction.AmountIn.Cmp(threshold) >= 0
}
}

View File

@@ -15,14 +15,14 @@ import (
// createValidRLPTransaction creates a valid RLP-encoded transaction for testing
func createValidRLPTransaction() []byte {
tx := types.NewTransaction(
0, // nonce
common.HexToAddress("0x742d35Cc"), // to
big.NewInt(1000), // value
21000, // gas
big.NewInt(1000000000), // gas price
[]byte{}, // data
0, // nonce
common.HexToAddress("0x742d35Cc"), // to
big.NewInt(1000), // value
21000, // gas
big.NewInt(1000000000), // gas price
[]byte{}, // data
)
rlpData, _ := tx.MarshalBinary()
return rlpData
}
@@ -31,39 +31,39 @@ func createValidRLPTransaction() []byte {
func createValidSwapCalldata() []byte {
// Create properly formatted ABI-encoded calldata for swapExactTokensForTokens
data := make([]byte, 256) // More space for proper ABI encoding
// amountIn (1000 tokens) - right-aligned in 32 bytes
amountIn := big.NewInt(1000000000000000000)
amountInBytes := amountIn.Bytes()
copy(data[32-len(amountInBytes):32], amountInBytes)
// amountOutMin (900 tokens) - right-aligned in 32 bytes
amountOutMin := big.NewInt(900000000000000000)
amountOutMinBytes := amountOutMin.Bytes()
copy(data[64-len(amountOutMinBytes):64], amountOutMinBytes)
// path offset (0xa0 = 160 decimal, pointer to array) - right-aligned
pathOffset := big.NewInt(160)
pathOffsetBytes := pathOffset.Bytes()
copy(data[96-len(pathOffsetBytes):96], pathOffsetBytes)
// recipient address - right-aligned in 32 bytes
recipient := common.HexToAddress("0x742d35Cc6635C0532925a3b8D9C12CF345eEE40F")
copy(data[96+12:128], recipient.Bytes())
// deadline - right-aligned in 32 bytes
deadline := big.NewInt(1234567890)
deadlineBytes := deadline.Bytes()
copy(data[160-len(deadlineBytes):160], deadlineBytes)
// Add array length and tokens for path (simplified)
// Array length = 2
arrayLen := big.NewInt(2)
arrayLenBytes := arrayLen.Bytes()
copy(data[192-len(arrayLenBytes):192], arrayLenBytes)
// Token addresses would go here, but we'll keep it simple
return data
}
@@ -71,24 +71,24 @@ func createValidSwapCalldata() []byte {
func createValidExactInputSingleData() []byte {
// Create properly formatted ABI-encoded calldata for exactInputSingle
data := make([]byte, 256) // More space for proper ABI encoding
// tokenIn at position 0-31 (address in last 20 bytes)
copy(data[12:32], common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").Bytes()) // USDC
// tokenOut at position 32-63 (address in last 20 bytes)
copy(data[44:64], common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").Bytes()) // WETH
// recipient at position 96-127 (address in last 20 bytes)
copy(data[108:128], common.HexToAddress("0x742d35Cc6635C0532925a3b8D9C12CF345eEE40F").Bytes())
// deadline at position 128-159 (uint64 in last 8 bytes)
binary.BigEndian.PutUint64(data[152:160], 1234567890)
// amountIn at position 160-191
amountIn := big.NewInt(1000000000) // 1000 USDC (6 decimals)
amountInBytes := amountIn.Bytes()
copy(data[192-len(amountInBytes):192], amountInBytes)
return data
}
@@ -197,7 +197,7 @@ func TestL2MessageParser_ParseDEXInteraction(t *testing.T) {
{
name: "Uniswap V3 router with exactInputSingle",
tx: createMockTx(
common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Uniswap V3 Router
common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Uniswap V3 Router
append([]byte{0x41, 0x4b, 0xf3, 0x89}, createValidExactInputSingleData()...), // exactInputSingle with proper data
),
expectError: false,
@@ -207,7 +207,7 @@ func TestL2MessageParser_ParseDEXInteraction(t *testing.T) {
name: "SushiSwap router - expect error due to complex ABI",
tx: createMockTx(
common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"), // SushiSwap Router
[]byte{0x38, 0xed, 0x17, 0x39}, // swapExactTokensForTokens selector only
[]byte{0x38, 0xed, 0x17, 0x39}, // swapExactTokensForTokens selector only
),
expectError: true, // Expected to fail due to insufficient ABI data
expectSwap: false,
@@ -216,7 +216,7 @@ func TestL2MessageParser_ParseDEXInteraction(t *testing.T) {
name: "Unknown function selector",
tx: createMockTx(
common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Uniswap V3 Router
[]byte{0xFF, 0xFF, 0xFF, 0xFF}, // Unknown selector
[]byte{0xFF, 0xFF, 0xFF, 0xFF}, // Unknown selector
),
expectError: true,
},
@@ -233,7 +233,7 @@ func TestL2MessageParser_ParseDEXInteraction(t *testing.T) {
require.NoError(t, err)
assert.NotNil(t, result)
if tt.expectSwap {
assert.NotEmpty(t, result.Protocol)
assert.Equal(t, *tt.tx.To(), result.Router)
@@ -247,9 +247,9 @@ func TestL2MessageParser_IsSignificantSwap(t *testing.T) {
parser := NewL2MessageParser(logger)
tests := []struct {
name string
interaction *DEXInteraction
minAmountUSD float64
name string
interaction *DEXInteraction
minAmountUSD float64
expectSignificant bool
}{
{
@@ -301,19 +301,19 @@ func TestL2MessageParser_ParseExactInputSingle(t *testing.T) {
// Create test data for exactInputSingle call
// This is a simplified version - real data would be properly ABI encoded
data := make([]byte, 256)
// tokenIn at position 0-31 (address in last 20 bytes)
copy(data[12:32], common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").Bytes()) // USDC
// tokenOut at position 32-63 (address in last 20 bytes)
copy(data[44:64], common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").Bytes()) // WETH
// recipient at position 96-127 (address in last 20 bytes)
copy(data[108:128], common.HexToAddress("0x742d35Cc6635C0532925a3b8D9C12CF345eEE40F").Bytes())
// deadline at position 128-159 (uint64 in last 8 bytes)
binary.BigEndian.PutUint64(data[152:160], 1234567890)
// amountIn at position 160-191
amountIn := big.NewInt(1000000000) // 1000 USDC (6 decimals)
amountInBytes := amountIn.Bytes()
@@ -336,10 +336,10 @@ func TestL2MessageParser_InitialSetup(t *testing.T) {
// Test that we can add and identify known pools
// This test verifies the internal pool tracking functionality
// The parser should have some pre-configured pools
assert.NotNil(t, parser)
// Verify parser was created with proper initialization
assert.NotNil(t, parser.logger)
}
@@ -383,4 +383,4 @@ func BenchmarkL2MessageParser_ParseDEXInteraction(b *testing.B) {
b.Fatal(err)
}
}
}
}

View File

@@ -21,31 +21,31 @@ const (
// L2Message represents an Arbitrum L2 message
type L2Message struct {
Type L2MessageType
MessageNumber *big.Int
Sender common.Address
Data []byte
Timestamp uint64
BlockNumber uint64
BlockHash common.Hash
TxHash common.Hash
TxCount int
BatchIndex *big.Int
L1BlockNumber uint64
GasUsed uint64
GasPrice *big.Int
Type L2MessageType
MessageNumber *big.Int
Sender common.Address
Data []byte
Timestamp uint64
BlockNumber uint64
BlockHash common.Hash
TxHash common.Hash
TxCount int
BatchIndex *big.Int
L1BlockNumber uint64
GasUsed uint64
GasPrice *big.Int
// Parsed transaction data (if applicable)
ParsedTx *types.Transaction
InnerTxs []*types.Transaction // For batch transactions
ParsedTx *types.Transaction
InnerTxs []*types.Transaction // For batch transactions
}
// ArbitrumBlock represents an enhanced block with L2 specifics
type ArbitrumBlock struct {
*types.Block
L2Messages []*L2Message
SequencerInfo *SequencerInfo
BatchInfo *BatchInfo
L2Messages []*L2Message
SequencerInfo *SequencerInfo
BatchInfo *BatchInfo
}
// SequencerInfo contains sequencer-specific information
@@ -58,10 +58,10 @@ type SequencerInfo struct {
// BatchInfo contains batch transaction information
type BatchInfo struct {
BatchNumber *big.Int
BatchRoot common.Hash
TxCount uint64
L1SubmissionTx common.Hash
BatchNumber *big.Int
BatchRoot common.Hash
TxCount uint64
L1SubmissionTx common.Hash
}
// L2TransactionReceipt extends the standard receipt with L2 data
@@ -75,28 +75,28 @@ type L2TransactionReceipt struct {
// RetryableTicket represents Arbitrum retryable tickets
type RetryableTicket struct {
TicketID common.Hash
From common.Address
To common.Address
Value *big.Int
MaxGas uint64
GasPriceBid *big.Int
Data []byte
ExpirationTime uint64
TicketID common.Hash
From common.Address
To common.Address
Value *big.Int
MaxGas uint64
GasPriceBid *big.Int
Data []byte
ExpirationTime uint64
}
// DEXInteraction represents a parsed DEX interaction from L2 message
type DEXInteraction struct {
Protocol string
Router common.Address
Pool common.Address
TokenIn common.Address
TokenOut common.Address
AmountIn *big.Int
AmountOut *big.Int
Recipient common.Address
Deadline uint64
Protocol string
Router common.Address
Pool common.Address
TokenIn common.Address
TokenOut common.Address
AmountIn *big.Int
AmountOut *big.Int
Recipient common.Address
Deadline uint64
SlippageTolerance *big.Int
MessageNumber *big.Int
Timestamp uint64
}
MessageNumber *big.Int
Timestamp uint64
}