Files
mev-beta/pkg/sequencer/decoder.go
Administrator fab8741544 refactor(config): use external DEX config in decoder
Replaced hardcoded router map with externalized DEX configuration from
config/dex.yaml for flexibility and maintainability.

## Changes Made

### pkg/sequencer/decoder.go
- Added pkg/config import
- Added package-level dexConfig variable
- Created InitDEXConfig() function to load config from file
- Modified GetSwapProtocol() to use config.Routers map instead of hardcoded map
- Removed 12 hardcoded router addresses
- Config fallback: uses function selector matching if config not loaded

## Benefits
- Configuration external from code
- Easy to add new DEX routers without code changes
- Centralized router configuration in config/dex.yaml
- Backward compatible: falls back to selector matching

## Usage
```go
// At startup:
if err := sequencer.InitDEXConfig("config/dex.yaml"); err != nil {
    log.Fatal(err)
}
```

## Testing
-  Compilation verified: go build ./pkg/sequencer/...
-  Backward compatible with existing code
-  Config loading from config/dex.yaml

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 13:57:44 +01:00

313 lines
8.2 KiB
Go

package sequencer
import (
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/your-org/mev-bot/pkg/config"
"github.com/your-org/mev-bot/pkg/validation"
)
// Package-level DEX configuration
var dexConfig *config.DEXConfig
// InitDEXConfig loads the DEX configuration from file
func InitDEXConfig(configPath string) error {
cfg, err := config.LoadDEXConfig(configPath)
if err != nil {
return fmt.Errorf("failed to load DEX config: %w", err)
}
dexConfig = cfg
return nil
}
// L2MessageKind represents the type of L2 message
type L2MessageKind uint8
const (
L2MessageKind_SignedTx L2MessageKind = 4
L2MessageKind_Batch L2MessageKind = 3
L2MessageKind_SignedCompressedTx L2MessageKind = 7
)
// ArbitrumMessage represents a decoded Arbitrum sequencer message
type ArbitrumMessage struct {
SequenceNumber uint64
Kind uint8
BlockNumber uint64
Timestamp uint64
L2MsgRaw string // Base64 encoded
Transaction *DecodedTransaction
}
// DecodedTransaction represents a decoded Arbitrum transaction
type DecodedTransaction struct {
Hash common.Hash
From common.Address
To *common.Address
Value *big.Int
Data []byte
Nonce uint64
GasPrice *big.Int
GasLimit uint64
RawBytes []byte // RLP-encoded transaction bytes for reconstruction
}
// DecodeArbitrumMessage decodes an Arbitrum sequencer feed message
func DecodeArbitrumMessage(msgMap map[string]interface{}) (*ArbitrumMessage, error) {
msg := &ArbitrumMessage{}
// Extract sequence number
if seqNum, ok := msgMap["sequenceNumber"].(float64); ok {
msg.SequenceNumber = uint64(seqNum)
}
// Extract nested message structure
messageWrapper, ok := msgMap["message"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("missing message wrapper")
}
message, ok := messageWrapper["message"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("missing inner message")
}
// Extract header
if header, ok := message["header"].(map[string]interface{}); ok {
if kind, ok := header["kind"].(float64); ok {
msg.Kind = uint8(kind)
}
if blockNum, ok := header["blockNumber"].(float64); ok {
msg.BlockNumber = uint64(blockNum)
}
if timestamp, ok := header["timestamp"].(float64); ok {
msg.Timestamp = uint64(timestamp)
}
}
// Extract l2Msg
l2MsgBase64, ok := message["l2Msg"].(string)
if !ok {
return nil, fmt.Errorf("missing l2Msg")
}
msg.L2MsgRaw = l2MsgBase64
// Decode transaction if it's a signed transaction (kind 3 from header means L1MessageType_L2Message)
if msg.Kind == 3 {
tx, err := DecodeL2Transaction(l2MsgBase64)
if err != nil {
// Not all messages are transactions, just skip
return msg, nil
}
msg.Transaction = tx
}
return msg, nil
}
// DecodeL2Transaction decodes a base64-encoded L2 transaction
func DecodeL2Transaction(l2MsgBase64 string) (*DecodedTransaction, error) {
// Step 1: Base64 decode
decoded, err := base64.StdEncoding.DecodeString(l2MsgBase64)
if err != nil {
return nil, fmt.Errorf("base64 decode failed: %w", err)
}
if len(decoded) == 0 {
return nil, fmt.Errorf("empty decoded message")
}
// Step 2: First byte is L2MessageKind
l2Kind := L2MessageKind(decoded[0])
// Only process signed transactions
if l2Kind != L2MessageKind_SignedTx {
return nil, fmt.Errorf("not a signed transaction (kind=%d)", l2Kind)
}
// Step 3: Strip first byte and RLP decode the transaction
txBytes := decoded[1:]
if len(txBytes) == 0 {
return nil, fmt.Errorf("empty transaction bytes")
}
// Try to decode as Ethereum transaction
tx := new(types.Transaction)
if err := rlp.DecodeBytes(txBytes, tx); err != nil {
return nil, fmt.Errorf("RLP decode failed: %w", err)
}
// Calculate transaction hash
txHash := crypto.Keccak256Hash(txBytes)
// Extract sender (requires chainID for EIP-155)
// For now, we'll skip sender recovery as it requires the chain ID
// and signature verification. We're mainly interested in To and Data.
result := &DecodedTransaction{
Hash: txHash,
To: tx.To(),
Value: tx.Value(),
Data: tx.Data(),
Nonce: tx.Nonce(),
GasPrice: tx.GasPrice(),
GasLimit: tx.Gas(),
RawBytes: txBytes, // Store for later reconstruction
}
return result, nil
}
// ToEthereumTransaction converts a DecodedTransaction back to *types.Transaction
// This is a reusable utility for converting our decoded format to go-ethereum format
func (dt *DecodedTransaction) ToEthereumTransaction() (*types.Transaction, error) {
if len(dt.RawBytes) == 0 {
return nil, fmt.Errorf("no raw transaction bytes available")
}
tx := new(types.Transaction)
if err := rlp.DecodeBytes(dt.RawBytes, tx); err != nil {
return nil, fmt.Errorf("failed to decode transaction: %w", err)
}
return tx, nil
}
// IsSwapTransaction checks if the transaction data is a DEX swap
func IsSwapTransaction(data []byte) bool {
if len(data) < 4 {
return false
}
// Extract function selector (first 4 bytes)
selector := hex.EncodeToString(data[0:4])
// Common DEX swap function selectors
swapSelectors := map[string]string{
// UniswapV2 Router
"38ed1739": "swapExactTokensForTokens",
"8803dbee": "swapTokensForExactTokens",
"7ff36ab5": "swapExactETHForTokens",
"fb3bdb41": "swapETHForExactTokens",
"18cbafe5": "swapExactTokensForETH",
"4a25d94a": "swapTokensForExactETH",
// UniswapV3 Router
"414bf389": "exactInputSingle",
"c04b8d59": "exactInput",
"db3e2198": "exactOutputSingle",
"f28c0498": "exactOutput",
// UniswapV2 Pair (direct swap)
"022c0d9f": "swap",
// Curve
"3df02124": "exchange",
"a6417ed6": "exchange_underlying",
// 1inch
"7c025200": "swap",
"e449022e": "uniswapV3Swap",
// 0x Protocol
"d9627aa4": "sellToUniswap",
"415565b0": "fillRfqOrder",
}
_, isSwap := swapSelectors[selector]
return isSwap
}
// DEXProtocol represents a DEX protocol
type DEXProtocol struct {
Name string
Version string
Type string // "router" or "pool"
}
// GetSwapProtocol identifies the DEX protocol from transaction data
func GetSwapProtocol(to *common.Address, data []byte) *DEXProtocol {
if to == nil || len(data) < 4 {
return &DEXProtocol{Name: "unknown", Version: "", Type: ""}
}
// Validate address is not zero
if err := validation.ValidateAddressPtr(to); err != nil {
return &DEXProtocol{Name: "unknown", Version: "", Type: ""}
}
selector := hex.EncodeToString(data[0:4])
toAddr := to.Hex()
// Check if it's a known router (from config if loaded, else use fallback)
if dexConfig != nil {
for addr, routerCfg := range dexConfig.Routers {
if addr == toAddr {
return &DEXProtocol{
Name: routerCfg.Name,
Version: routerCfg.Version,
Type: routerCfg.Type,
}
}
}
}
// Try to identify by function selector
switch selector {
// UniswapV2-style swap
case "022c0d9f":
return &DEXProtocol{Name: "UniswapV2", Version: "", Type: "pool"}
// UniswapV2 Router
case "38ed1739", "8803dbee", "7ff36ab5", "fb3bdb41", "18cbafe5", "4a25d94a":
return &DEXProtocol{Name: "UniswapV2", Version: "", Type: "router"}
// UniswapV3 Router
case "414bf389", "c04b8d59", "db3e2198", "f28c0498", "5ae401dc", "ac9650d8":
return &DEXProtocol{Name: "UniswapV3", Version: "", Type: "router"}
// Curve
case "3df02124", "a6417ed6", "394747c5", "5b41b908":
return &DEXProtocol{Name: "Curve", Version: "", Type: "pool"}
// Balancer
case "52bbbe29": // swap
return &DEXProtocol{Name: "Balancer", Version: "V2", Type: "vault"}
// Camelot V3 swap
case "128acb08": // exactInputSingle for Camelot V3
return &DEXProtocol{Name: "Camelot", Version: "V3", Type: "router"}
default:
return &DEXProtocol{Name: "unknown", Version: "", Type: ""}
}
}
// IsSupportedDEX checks if the protocol is one we want to track
func IsSupportedDEX(protocol *DEXProtocol) bool {
if protocol == nil {
return false
}
supportedDEXes := map[string]bool{
"UniswapV2": true,
"UniswapV3": true,
"UniswapUniversal": true,
"SushiSwap": true,
"Camelot": true,
"Balancer": true,
"Curve": true,
"KyberSwap": true,
}
return supportedDEXes[protocol.Name]
}