Sequencer is working (minimal parsing)
This commit is contained in:
428
pkg/arbitrum/client.go
Normal file
428
pkg/arbitrum/client.go
Normal file
@@ -0,0 +1,428 @@
|
||||
package arbitrum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// ArbitrumClient extends the standard Ethereum client with Arbitrum-specific functionality
|
||||
type ArbitrumClient struct {
|
||||
*ethclient.Client
|
||||
rpcClient *rpc.Client
|
||||
Logger *logger.Logger
|
||||
ChainID *big.Int
|
||||
}
|
||||
|
||||
// NewArbitrumClient creates a new Arbitrum-specific client
|
||||
func NewArbitrumClient(endpoint string, logger *logger.Logger) (*ArbitrumClient, error) {
|
||||
rpcClient, err := rpc.Dial(endpoint)
|
||||
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,
|
||||
Logger: logger,
|
||||
ChainID: chainID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SubscribeToL2Messages subscribes to L2 message events
|
||||
func (c *ArbitrumClient) SubscribeToL2Messages(ctx context.Context, ch chan<- *L2Message) (ethereum.Subscription, error) {
|
||||
// Validate inputs
|
||||
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() {
|
||||
// Recover from potential panic when closing channel
|
||||
if r := recover(); r != nil {
|
||||
c.Logger.Error(fmt.Sprintf("Panic while closing L2 message channel: %v", r))
|
||||
}
|
||||
// Safely close the channel
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.Logger.Debug("L2 message channel already closed")
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Context cancelled, don't close channel as it might be used elsewhere
|
||||
default:
|
||||
close(ch)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case header := <-headers:
|
||||
if header != nil {
|
||||
if err := c.processBlockForL2Messages(ctx, header, ch); err != nil {
|
||||
c.Logger.Error(fmt.Sprintf("Error processing block %d for L2 messages: %v", header.Number.Uint64(), err))
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// processBlockForL2Messages processes a block to extract L2 messages
|
||||
func (c *ArbitrumClient) processBlockForL2Messages(ctx context.Context, header *types.Header, ch chan<- *L2Message) error {
|
||||
// Validate inputs
|
||||
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{
|
||||
Type: L2Transaction, // Treat each block as containing transaction data
|
||||
MessageNumber: header.Number,
|
||||
Data: c.encodeBlockAsL2Message(header),
|
||||
Timestamp: header.Time,
|
||||
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 {
|
||||
c.Logger.Debug(fmt.Sprintf("Could not fetch full block %d, using header only: %v", header.Number.Uint64(), err))
|
||||
} 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 {
|
||||
// Create a summary message with transaction data
|
||||
l2Message.Data = c.encodeTransactionsAsL2Message(block.Transactions())
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- l2Message:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeBlockAsL2Message creates a simple L2 message encoding from a block 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
|
||||
}
|
||||
|
||||
// encodeTransactionsAsL2Message creates an encoding from transaction list
|
||||
func (c *ArbitrumClient) encodeTransactionsAsL2Message(transactions []*types.Transaction) []byte {
|
||||
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
|
||||
}
|
||||
|
||||
// extractL2MessageFromTransaction extracts L2 message data from a transaction
|
||||
func (c *ArbitrumClient) extractL2MessageFromTransaction(tx *types.Transaction, timestamp uint64) *L2Message {
|
||||
// Check if this transaction contains L2 message data
|
||||
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,
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetL2TransactionReceipt gets the receipt for an L2 transaction with additional data
|
||||
func (c *ArbitrumClient) GetL2TransactionReceipt(ctx context.Context, txHash common.Hash) (*L2TransactionReceipt, error) {
|
||||
receipt, err := c.TransactionReceipt(ctx, txHash)
|
||||
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
|
||||
}
|
||||
|
||||
// enrichL2Receipt adds L2-specific data to the receipt
|
||||
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) {
|
||||
ticket, err := c.parseRetryableTicket(log)
|
||||
if err == nil {
|
||||
receipt.RetryableTicket = ticket
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isRetryableTicketLog checks if a log represents a retryable ticket
|
||||
func (c *ArbitrumClient) isRetryableTicketLog(log *types.Log) bool {
|
||||
// Retryable ticket creation signature
|
||||
retryableTicketSig := common.HexToHash("0xb4df3847300f076a369cd76d2314b470a1194d9e8a6bb97f1860aee88a5f6748")
|
||||
return len(log.Topics) > 0 && log.Topics[0] == retryableTicketSig
|
||||
}
|
||||
|
||||
// parseRetryableTicket parses retryable ticket data from a log
|
||||
func (c *ArbitrumClient) parseRetryableTicket(log *types.Log) (*RetryableTicket, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// GetL2MessageByNumber gets an L2 message by its number
|
||||
func (c *ArbitrumClient) GetL2MessageByNumber(ctx context.Context, messageNumber *big.Int) (*L2Message, error) {
|
||||
// This would use Arbitrum-specific RPC methods
|
||||
var result map[string]interface{}
|
||||
err := c.rpcClient.CallContext(ctx, &result, "arb_getL2ToL1Msg", 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
|
||||
}
|
||||
|
||||
// GetBatchByNumber gets a batch by its number
|
||||
func (c *ArbitrumClient) GetBatchByNumber(ctx context.Context, batchNumber *big.Int) (*BatchInfo, error) {
|
||||
var result map[string]interface{}
|
||||
err := c.rpcClient.CallContext(ctx, &result, "arb_getBatch", batchNumber)
|
||||
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
|
||||
}
|
||||
|
||||
// SubscribeToNewBatches subscribes to new batch submissions
|
||||
func (c *ArbitrumClient) SubscribeToNewBatches(ctx context.Context, ch chan<- *BatchInfo) (ethereum.Subscription, error) {
|
||||
// Create filter for batch submission events
|
||||
query := ethereum.FilterQuery{
|
||||
Addresses: []common.Address{
|
||||
common.HexToAddress("0x1c479675ad559DC151F6Ec7ed3FbF8ceE79582B6"), // Sequencer Inbox
|
||||
},
|
||||
Topics: [][]common.Hash{
|
||||
{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)
|
||||
for {
|
||||
select {
|
||||
case log := <-logs:
|
||||
if batch := c.parseBatchFromLog(log); batch != nil {
|
||||
select {
|
||||
case ch <- batch:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// parseBatchFromLog parses batch information from a log event
|
||||
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
|
||||
}
|
||||
|
||||
// Close closes the Arbitrum client
|
||||
func (c *ArbitrumClient) Close() {
|
||||
c.Client.Close()
|
||||
c.rpcClient.Close()
|
||||
}
|
||||
292
pkg/arbitrum/gas.go
Normal file
292
pkg/arbitrum/gas.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package arbitrum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// L2GasEstimator provides Arbitrum-specific gas estimation and optimization
|
||||
type L2GasEstimator struct {
|
||||
client *ArbitrumClient
|
||||
logger *logger.Logger
|
||||
|
||||
// L2 gas price configuration
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// EstimateL2Gas provides comprehensive gas estimation for L2 transactions
|
||||
func (g *L2GasEstimator) EstimateL2Gas(ctx context.Context, tx *types.Transaction) (*GasEstimate, error) {
|
||||
// Get current gas price data
|
||||
gasPrice, err := g.client.SuggestGasPrice(ctx)
|
||||
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,
|
||||
MaxPriorityFee: priorityFee,
|
||||
L1DataFee: l1DataFee,
|
||||
L2ComputeFee: l2ComputeFee,
|
||||
TotalFee: totalFee,
|
||||
Confidence: g.calculateConfidence(gasPrice, priorityFee),
|
||||
}
|
||||
|
||||
return estimate, nil
|
||||
}
|
||||
|
||||
// estimateGasLimit estimates the gas limit for an L2 transaction
|
||||
func (g *L2GasEstimator) estimateGasLimit(ctx context.Context, tx *types.Transaction) (uint64, error) {
|
||||
// Create a call message for gas estimation
|
||||
msg := ethereum.CallMsg{
|
||||
From: common.Address{}, // Will be overridden
|
||||
To: tx.To(),
|
||||
Value: tx.Value(),
|
||||
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
|
||||
}
|
||||
|
||||
// estimateL1DataFee calculates the L1 data fee component (Arbitrum-specific)
|
||||
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++
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// calculateOptimalPriorityFee calculates an optimal priority fee for fast inclusion
|
||||
func (g *L2GasEstimator) calculateOptimalPriorityFee(ctx context.Context, baseFee *big.Int) *big.Int {
|
||||
// Try to get recent priority fees from the network
|
||||
priorityFee, err := g.getSuggestedPriorityFee(ctx)
|
||||
if err != nil {
|
||||
// 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)
|
||||
}
|
||||
if priorityFee.Cmp(g.priorityFeeMax) > 0 {
|
||||
priorityFee = new(big.Int).Set(g.priorityFeeMax)
|
||||
}
|
||||
|
||||
return priorityFee
|
||||
}
|
||||
|
||||
// getSuggestedPriorityFee gets suggested priority fee from the network
|
||||
func (g *L2GasEstimator) getSuggestedPriorityFee(ctx context.Context) (*big.Int, error) {
|
||||
// Use eth_maxPriorityFeePerGas if available
|
||||
var result string
|
||||
err := g.client.rpcClient.CallContext(ctx, &result, "eth_maxPriorityFeePerGas")
|
||||
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
|
||||
}
|
||||
|
||||
// calculateConfidence calculates confidence level for the gas estimate
|
||||
func (g *L2GasEstimator) calculateConfidence(gasPrice, priorityFee *big.Int) float64 {
|
||||
// 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 {
|
||||
confidence = 1.0
|
||||
}
|
||||
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
|
||||
return 21000
|
||||
case dataSize < 100:
|
||||
// Simple contract interaction
|
||||
return 50000
|
||||
case dataSize < 1000:
|
||||
// Complex contract interaction
|
||||
return 150000
|
||||
case dataSize < 5000:
|
||||
// Very complex interaction (e.g., DEX swap)
|
||||
return 300000
|
||||
default:
|
||||
// Extremely complex interaction
|
||||
return 500000
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IsL2TransactionViable checks if an L2 transaction is economically viable
|
||||
func (g *L2GasEstimator) IsL2TransactionViable(estimate *GasEstimate, expectedProfit *big.Int) bool {
|
||||
// Compare total fee to expected profit
|
||||
return estimate.TotalFee.Cmp(expectedProfit) < 0
|
||||
}
|
||||
|
||||
343
pkg/arbitrum/l2_parser.go
Normal file
343
pkg/arbitrum/l2_parser.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package arbitrum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// RawL2Transaction represents a raw Arbitrum L2 transaction
|
||||
type RawL2Transaction 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"`
|
||||
Nonce string `json:"nonce"`
|
||||
TransactionIndex string `json:"transactionIndex"`
|
||||
Type string `json:"type"`
|
||||
ChainID string `json:"chainId,omitempty"`
|
||||
V string `json:"v,omitempty"`
|
||||
R string `json:"r,omitempty"`
|
||||
S string `json:"s,omitempty"`
|
||||
}
|
||||
|
||||
// RawL2Block represents a raw Arbitrum L2 block
|
||||
type RawL2Block struct {
|
||||
Hash string `json:"hash"`
|
||||
Number string `json:"number"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Transactions []RawL2Transaction `json:"transactions"`
|
||||
}
|
||||
|
||||
// DEXFunctionSignature represents a DEX function signature
|
||||
type DEXFunctionSignature struct {
|
||||
Signature string
|
||||
Name string
|
||||
Protocol string
|
||||
Description string
|
||||
}
|
||||
|
||||
// ArbitrumL2Parser handles parsing of Arbitrum L2 transactions
|
||||
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
|
||||
}
|
||||
|
||||
// NewArbitrumL2Parser creates a new Arbitrum L2 transaction parser
|
||||
func NewArbitrumL2Parser(rpcEndpoint string, logger *logger.Logger) (*ArbitrumL2Parser, error) {
|
||||
client, err := rpc.Dial(rpcEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Arbitrum RPC: %v", err)
|
||||
}
|
||||
|
||||
parser := &ArbitrumL2Parser{
|
||||
client: client,
|
||||
logger: logger,
|
||||
dexContracts: make(map[common.Address]string),
|
||||
dexFunctions: make(map[string]DEXFunctionSignature),
|
||||
}
|
||||
|
||||
// Initialize DEX contracts and functions
|
||||
parser.initializeDEXData()
|
||||
|
||||
return parser, nil
|
||||
}
|
||||
|
||||
// initializeDEXData initializes known DEX contracts and function signatures
|
||||
func (p *ArbitrumL2Parser) initializeDEXData() {
|
||||
// Official Arbitrum DEX contracts
|
||||
p.dexContracts[common.HexToAddress("0xf1D7CC64Fb4452F05c498126312eBE29f30Fbcf9")] = "UniswapV2Factory"
|
||||
p.dexContracts[common.HexToAddress("0x1F98431c8aD98523631AE4a59f267346ea31F984")] = "UniswapV3Factory"
|
||||
p.dexContracts[common.HexToAddress("0xc35DADB65012eC5796536bD9864eD8773aBc74C4")] = "SushiSwapFactory"
|
||||
p.dexContracts[common.HexToAddress("0x4752ba5dbc23f44d87826276bf6fd6b1c372ad24")] = "UniswapV2Router02"
|
||||
p.dexContracts[common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")] = "UniswapV3Router"
|
||||
p.dexContracts[common.HexToAddress("0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45")] = "UniswapV3Router02"
|
||||
p.dexContracts[common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506")] = "SushiSwapRouter"
|
||||
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",
|
||||
Name: "swapExactTokensForTokens",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap exact tokens for tokens",
|
||||
}
|
||||
p.dexFunctions["0x8803dbee"] = DEXFunctionSignature{
|
||||
Signature: "0x8803dbee",
|
||||
Name: "swapTokensForExactTokens",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap tokens for exact tokens",
|
||||
}
|
||||
p.dexFunctions["0x7ff36ab5"] = DEXFunctionSignature{
|
||||
Signature: "0x7ff36ab5",
|
||||
Name: "swapExactETHForTokens",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap exact ETH for tokens",
|
||||
}
|
||||
p.dexFunctions["0x4a25d94a"] = DEXFunctionSignature{
|
||||
Signature: "0x4a25d94a",
|
||||
Name: "swapTokensForExactETH",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap tokens for exact ETH",
|
||||
}
|
||||
p.dexFunctions["0x18cbafe5"] = DEXFunctionSignature{
|
||||
Signature: "0x18cbafe5",
|
||||
Name: "swapExactTokensForETH",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap exact tokens for ETH",
|
||||
}
|
||||
p.dexFunctions["0x791ac947"] = DEXFunctionSignature{
|
||||
Signature: "0x791ac947",
|
||||
Name: "swapExactTokensForETHSupportingFeeOnTransferTokens",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap exact tokens for ETH supporting fee-on-transfer tokens",
|
||||
}
|
||||
p.dexFunctions["0xb6f9de95"] = DEXFunctionSignature{
|
||||
Signature: "0xb6f9de95",
|
||||
Name: "swapExactETHForTokensSupportingFeeOnTransferTokens",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap exact ETH for tokens supporting fee-on-transfer tokens",
|
||||
}
|
||||
p.dexFunctions["0x5c11d795"] = DEXFunctionSignature{
|
||||
Signature: "0x5c11d795",
|
||||
Name: "swapExactTokensForTokensSupportingFeeOnTransferTokens",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Swap exact tokens for tokens supporting fee-on-transfer tokens",
|
||||
}
|
||||
|
||||
// Uniswap V2 liquidity functions
|
||||
p.dexFunctions["0xe8e33700"] = DEXFunctionSignature{
|
||||
Signature: "0xe8e33700",
|
||||
Name: "addLiquidity",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Add liquidity to pool",
|
||||
}
|
||||
p.dexFunctions["0xf305d719"] = DEXFunctionSignature{
|
||||
Signature: "0xf305d719",
|
||||
Name: "addLiquidityETH",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Add liquidity with ETH",
|
||||
}
|
||||
p.dexFunctions["0xbaa2abde"] = DEXFunctionSignature{
|
||||
Signature: "0xbaa2abde",
|
||||
Name: "removeLiquidity",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Remove liquidity from pool",
|
||||
}
|
||||
p.dexFunctions["0x02751cec"] = DEXFunctionSignature{
|
||||
Signature: "0x02751cec",
|
||||
Name: "removeLiquidityETH",
|
||||
Protocol: "UniswapV2",
|
||||
Description: "Remove liquidity with ETH",
|
||||
}
|
||||
|
||||
// Uniswap V3 swap functions
|
||||
p.dexFunctions["0x414bf389"] = DEXFunctionSignature{
|
||||
Signature: "0x414bf389",
|
||||
Name: "exactInputSingle",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Exact input single swap",
|
||||
}
|
||||
p.dexFunctions["0xc04b8d59"] = DEXFunctionSignature{
|
||||
Signature: "0xc04b8d59",
|
||||
Name: "exactInput",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Exact input multi-hop swap",
|
||||
}
|
||||
p.dexFunctions["0xdb3e2198"] = DEXFunctionSignature{
|
||||
Signature: "0xdb3e2198",
|
||||
Name: "exactOutputSingle",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Exact output single swap",
|
||||
}
|
||||
p.dexFunctions["0xf28c0498"] = DEXFunctionSignature{
|
||||
Signature: "0xf28c0498",
|
||||
Name: "exactOutput",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Exact output multi-hop swap",
|
||||
}
|
||||
p.dexFunctions["0xac9650d8"] = DEXFunctionSignature{
|
||||
Signature: "0xac9650d8",
|
||||
Name: "multicall",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Batch multiple function calls",
|
||||
}
|
||||
|
||||
// Uniswap V3 position management functions
|
||||
p.dexFunctions["0x88316456"] = DEXFunctionSignature{
|
||||
Signature: "0x88316456",
|
||||
Name: "mint",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Mint new liquidity position",
|
||||
}
|
||||
p.dexFunctions["0xfc6f7865"] = DEXFunctionSignature{
|
||||
Signature: "0xfc6f7865",
|
||||
Name: "collect",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Collect fees from position",
|
||||
}
|
||||
p.dexFunctions["0x219f5d17"] = DEXFunctionSignature{
|
||||
Signature: "0x219f5d17",
|
||||
Name: "increaseLiquidity",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Increase liquidity in position",
|
||||
}
|
||||
p.dexFunctions["0x0c49ccbe"] = DEXFunctionSignature{
|
||||
Signature: "0x0c49ccbe",
|
||||
Name: "decreaseLiquidity",
|
||||
Protocol: "UniswapV3",
|
||||
Description: "Decrease liquidity in position",
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, fmt.Errorf("failed to get block %d: %v", blockNumber, err)
|
||||
}
|
||||
|
||||
p.logger.Debug(fmt.Sprintf("Retrieved L2 block %d with %d transactions", blockNumber, len(block.Transactions)))
|
||||
return &block, nil
|
||||
}
|
||||
|
||||
// ParseDEXTransactions analyzes transactions in a block for DEX interactions
|
||||
func (p *ArbitrumL2Parser) ParseDEXTransactions(ctx context.Context, block *RawL2Block) []DEXTransaction {
|
||||
var dexTransactions []DEXTransaction
|
||||
|
||||
for _, tx := range block.Transactions {
|
||||
if dexTx := p.parseDEXTransaction(tx); dexTx != nil {
|
||||
dexTransactions = append(dexTransactions, *dexTx)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dexTransactions) > 0 {
|
||||
p.logger.Info(fmt.Sprintf("Block %s: Found %d DEX transactions", block.Number, len(dexTransactions)))
|
||||
}
|
||||
|
||||
return dexTransactions
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// parseDEXTransaction checks if a transaction is a DEX interaction
|
||||
func (p *ArbitrumL2Parser) parseDEXTransaction(tx RawL2Transaction) *DEXTransaction {
|
||||
// Skip transactions without recipient (contract creation)
|
||||
if tx.To == "" || tx.To == "0x" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip transactions without input data
|
||||
if tx.Input == "" || tx.Input == "0x" || len(tx.Input) < 10 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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" {
|
||||
value.SetString(strings.TrimPrefix(tx.Value, "0x"), 16)
|
||||
}
|
||||
|
||||
// Parse input data
|
||||
inputData, err := hex.DecodeString(strings.TrimPrefix(tx.Input, "0x"))
|
||||
if err != nil {
|
||||
p.logger.Debug(fmt.Sprintf("Failed to decode input data for transaction %s: %v", tx.Hash, err))
|
||||
inputData = []byte{}
|
||||
}
|
||||
|
||||
p.logger.Info(fmt.Sprintf("DEX Transaction detected: %s -> %s (%s) calling %s (%s), Value: %s ETH",
|
||||
tx.From, tx.To, contractName, funcInfo.Name, funcInfo.Protocol,
|
||||
new(big.Float).Quo(new(big.Float).SetInt(value), big.NewFloat(1e18)).String()))
|
||||
|
||||
return &DEXTransaction{
|
||||
Hash: tx.Hash,
|
||||
From: tx.From,
|
||||
To: tx.To,
|
||||
Value: value,
|
||||
FunctionSig: functionSig,
|
||||
FunctionName: funcInfo.Name,
|
||||
Protocol: funcInfo.Protocol,
|
||||
InputData: inputData,
|
||||
ContractName: contractName,
|
||||
BlockNumber: "", // Will be set by caller
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
tx.From, tx.To, contractName, functionSig))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the RPC connection
|
||||
func (p *ArbitrumL2Parser) Close() {
|
||||
if p.client != nil {
|
||||
p.client.Close()
|
||||
}
|
||||
}
|
||||
605
pkg/arbitrum/parser.go
Normal file
605
pkg/arbitrum/parser.go
Normal file
@@ -0,0 +1,605 @@
|
||||
package arbitrum
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// L2MessageParser parses Arbitrum L2 messages and transactions
|
||||
type L2MessageParser struct {
|
||||
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
|
||||
}
|
||||
|
||||
// NewL2MessageParser creates a new L2 message parser
|
||||
func NewL2MessageParser(logger *logger.Logger) *L2MessageParser {
|
||||
parser := &L2MessageParser{
|
||||
logger: logger,
|
||||
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
|
||||
}
|
||||
|
||||
// initializeKnownAddresses sets up known DEX addresses on Arbitrum
|
||||
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%"
|
||||
p.knownPools[common.HexToAddress("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640")] = "ETH/USDC-0.05%"
|
||||
p.knownPools[common.HexToAddress("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")] = "ETH/USDC-0.3%"
|
||||
}
|
||||
|
||||
// loadABIs loads the required ABI definitions
|
||||
func (p *L2MessageParser) loadABIs() {
|
||||
// Simplified ABI loading - in production, load from files
|
||||
uniswapV2RouterABI := `[
|
||||
{
|
||||
"inputs": [
|
||||
{"internalType": "uint256", "name": "amountIn", "type": "uint256"},
|
||||
{"internalType": "uint256", "name": "amountOutMin", "type": "uint256"},
|
||||
{"internalType": "address[]", "name": "path", "type": "address[]"},
|
||||
{"internalType": "address", "name": "to", "type": "address"},
|
||||
{"internalType": "uint256", "name": "deadline", "type": "uint256"}
|
||||
],
|
||||
"name": "swapExactTokensForTokens",
|
||||
"outputs": [{"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
}
|
||||
]`
|
||||
|
||||
var err error
|
||||
p.uniswapV2RouterABI, err = abi.JSON(bytes.NewReader([]byte(uniswapV2RouterABI)))
|
||||
if err != nil {
|
||||
p.logger.Error(fmt.Sprintf("Failed to load Uniswap V2 Router ABI: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// ParseL2Message parses an L2 message and extracts relevant information
|
||||
func (p *L2MessageParser) ParseL2Message(messageData []byte, messageNumber *big.Int, timestamp uint64) (*L2Message, error) {
|
||||
// Validate inputs
|
||||
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:])
|
||||
case 7: // Batch submission
|
||||
return p.parseL2Batch(l2Message, messageData[4:])
|
||||
default:
|
||||
p.logger.Debug(fmt.Sprintf("Unknown L2 message type: %d", msgType))
|
||||
return l2Message, nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseL2Transaction parses an L2 transaction message
|
||||
func (p *L2MessageParser) parseL2Transaction(l2Message *L2Message, data []byte) (*L2Message, error) {
|
||||
// Validate inputs
|
||||
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
|
||||
}
|
||||
|
||||
// parseL2Batch parses a batch submission message
|
||||
func (p *L2MessageParser) parseL2Batch(l2Message *L2Message, data []byte) (*L2Message, error) {
|
||||
// Validate inputs
|
||||
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 {
|
||||
// Incomplete data, log warning but continue with what we have
|
||||
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 {
|
||||
innerTxs = append(innerTxs, tx)
|
||||
} else {
|
||||
p.logger.Warn("Parsed nil transaction in batch")
|
||||
}
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
|
||||
// ParseDEXInteraction extracts DEX interaction details from a transaction
|
||||
func (p *L2MessageParser) ParseDEXInteraction(tx *types.Transaction) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
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
|
||||
if poolName, isPool := p.knownPools[to]; isPool {
|
||||
protocol = poolName
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// Parse based on function selector
|
||||
switch common.Bytes2Hex(selector) {
|
||||
case "38ed1739": // swapExactTokensForTokens (Uniswap V2)
|
||||
return p.parseSwapExactTokensForTokens(interaction, data[4:])
|
||||
case "8803dbee": // swapTokensForExactTokens (Uniswap V2)
|
||||
return p.parseSwapTokensForExactTokens(interaction, data[4:])
|
||||
case "18cbafe5": // swapExactTokensForTokensSupportingFeeOnTransferTokens (Uniswap V2)
|
||||
return p.parseSwapExactTokensForTokens(interaction, data[4:])
|
||||
case "414bf389": // exactInputSingle (Uniswap V3)
|
||||
return p.parseExactInputSingle(interaction, data[4:])
|
||||
case "db3e2198": // exactInput (Uniswap V3)
|
||||
return p.parseExactInput(interaction, data[4:])
|
||||
case "f305d719": // exactOutputSingle (Uniswap V3)
|
||||
return p.parseExactOutputSingle(interaction, data[4:])
|
||||
case "04e45aaf": // exactOutput (Uniswap V3)
|
||||
return p.parseExactOutput(interaction, data[4:])
|
||||
case "7ff36ab5": // swapExactETHForTokens (Uniswap V2)
|
||||
return p.parseSwapExactETHForTokens(interaction, data[4:])
|
||||
case "18cffa1c": // swapExactETHForTokensSupportingFeeOnTransferTokens (Uniswap V2)
|
||||
return p.parseSwapExactETHForTokens(interaction, data[4:])
|
||||
case "b6f9de95": // swapExactTokensForETH (Uniswap V2)
|
||||
return p.parseSwapExactTokensForETH(interaction, data[4:])
|
||||
case "791ac947": // swapExactTokensForETHSupportingFeeOnTransferTokens (Uniswap V2)
|
||||
return p.parseSwapExactTokensForETH(interaction, data[4:])
|
||||
case "5ae401dc": // multicall (Uniswap V3)
|
||||
return p.parseMulticall(interaction, data[4:])
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown DEX function selector: %s", common.Bytes2Hex(selector))
|
||||
}
|
||||
}
|
||||
|
||||
// parseSwapExactTokensForTokens parses Uniswap V2 style swap
|
||||
func (p *L2MessageParser) parseSwapExactTokensForTokens(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
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
|
||||
}
|
||||
|
||||
// parseSwapTokensForExactTokens parses exact output swaps
|
||||
func (p *L2MessageParser) parseSwapTokensForExactTokens(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Similar to above but for exact output
|
||||
// Implementation would be similar to parseSwapExactTokensForTokens
|
||||
// but with different parameter ordering
|
||||
return interaction, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// parseSwapExactETHForTokens parses ETH to token swaps
|
||||
func (p *L2MessageParser) parseSwapExactETHForTokens(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Implementation for ETH to token swaps
|
||||
return interaction, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// parseSwapExactTokensForETH parses token to ETH swaps
|
||||
func (p *L2MessageParser) parseSwapExactTokensForETH(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Implementation for token to ETH swaps
|
||||
return interaction, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// parseExactOutputSingle parses Uniswap V3 exact output single pool swap
|
||||
func (p *L2MessageParser) parseExactOutputSingle(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Implementation for exact output swaps
|
||||
return interaction, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// parseExactOutput parses Uniswap V3 exact output multi-hop swap
|
||||
func (p *L2MessageParser) parseExactOutput(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Implementation for exact output multi-hop swaps
|
||||
return interaction, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// parseMulticall parses Uniswap V3 multicall transactions
|
||||
func (p *L2MessageParser) parseMulticall(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Implementation for multicall transactions
|
||||
return interaction, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// parseExactInputSingle parses Uniswap V3 single pool swap
|
||||
func (p *L2MessageParser) parseExactInputSingle(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// Validate inputs
|
||||
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;
|
||||
// address tokenOut;
|
||||
// uint24 fee;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// uint256 amountIn;
|
||||
// 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])
|
||||
// Validate amount is reasonable (not negative)
|
||||
if amountIn.Sign() < 0 {
|
||||
return nil, fmt.Errorf("negative amountIn")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// parseExactInput parses Uniswap V3 multi-hop swap
|
||||
func (p *L2MessageParser) parseExactInput(interaction *DEXInteraction, data []byte) (*DEXInteraction, error) {
|
||||
// This would parse the more complex multi-hop swap structure
|
||||
return interaction, fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// IsSignificantSwap determines if a DEX interaction is significant enough to monitor
|
||||
func (p *L2MessageParser) IsSignificantSwap(interaction *DEXInteraction, minAmountUSD float64) bool {
|
||||
// Validate inputs
|
||||
if interaction == nil {
|
||||
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
|
||||
}
|
||||
386
pkg/arbitrum/parser_test.go
Normal file
386
pkg/arbitrum/parser_test.go
Normal file
@@ -0,0 +1,386 @@
|
||||
package arbitrum
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
rlpData, _ := tx.MarshalBinary()
|
||||
return rlpData
|
||||
}
|
||||
|
||||
// createValidSwapCalldata creates valid swap function calldata
|
||||
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
|
||||
}
|
||||
|
||||
// createValidExactInputSingleData creates valid exactInputSingle calldata
|
||||
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
|
||||
}
|
||||
|
||||
func TestL2MessageParser_ParseL2Message(t *testing.T) {
|
||||
logger := &logger.Logger{}
|
||||
parser := NewL2MessageParser(logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
messageData []byte
|
||||
messageNumber *big.Int
|
||||
timestamp uint64
|
||||
expectError bool
|
||||
expectedType L2MessageType
|
||||
}{
|
||||
{
|
||||
name: "Empty message",
|
||||
messageData: []byte{},
|
||||
messageNumber: big.NewInt(1),
|
||||
timestamp: 1234567890,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Short message",
|
||||
messageData: []byte{0x00, 0x00, 0x00},
|
||||
messageNumber: big.NewInt(2),
|
||||
timestamp: 1234567890,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "L2 Transaction message",
|
||||
messageData: append([]byte{0x00, 0x00, 0x00, 0x03}, createValidRLPTransaction()...),
|
||||
messageNumber: big.NewInt(3),
|
||||
timestamp: 1234567890,
|
||||
expectError: false,
|
||||
expectedType: L2Transaction,
|
||||
},
|
||||
{
|
||||
name: "L2 Batch message",
|
||||
messageData: append([]byte{0x00, 0x00, 0x00, 0x07}, make([]byte, 64)...),
|
||||
messageNumber: big.NewInt(4),
|
||||
timestamp: 1234567890,
|
||||
expectError: false,
|
||||
expectedType: L2BatchSubmission,
|
||||
},
|
||||
{
|
||||
name: "Unknown message type",
|
||||
messageData: append([]byte{0x00, 0x00, 0x00, 0xFF}, make([]byte, 32)...),
|
||||
messageNumber: big.NewInt(5),
|
||||
timestamp: 1234567890,
|
||||
expectError: false,
|
||||
expectedType: L2Unknown,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parser.ParseL2Message(tt.messageData, tt.messageNumber, tt.timestamp)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, tt.expectedType, result.Type)
|
||||
assert.Equal(t, tt.messageNumber, result.MessageNumber)
|
||||
assert.Equal(t, tt.timestamp, result.Timestamp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestL2MessageParser_ParseDEXInteraction(t *testing.T) {
|
||||
logger := &logger.Logger{}
|
||||
parser := NewL2MessageParser(logger)
|
||||
|
||||
// Create a mock transaction for testing
|
||||
createMockTx := func(to common.Address, data []byte) *types.Transaction {
|
||||
return types.NewTransaction(
|
||||
0,
|
||||
to,
|
||||
big.NewInt(0),
|
||||
21000,
|
||||
big.NewInt(1000000000),
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tx *types.Transaction
|
||||
expectError bool
|
||||
expectSwap bool
|
||||
}{
|
||||
{
|
||||
name: "Contract creation transaction",
|
||||
tx: types.NewContractCreation(0, big.NewInt(0), 21000, big.NewInt(1000000000), []byte{}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Unknown router address",
|
||||
tx: createMockTx(common.HexToAddress("0x1234567890123456789012345678901234567890"), []byte{0x38, 0xed, 0x17, 0x39}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Uniswap V3 router with exactInputSingle",
|
||||
tx: createMockTx(
|
||||
common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Uniswap V3 Router
|
||||
append([]byte{0x41, 0x4b, 0xf3, 0x89}, createValidExactInputSingleData()...), // exactInputSingle with proper data
|
||||
),
|
||||
expectError: false,
|
||||
expectSwap: true,
|
||||
},
|
||||
{
|
||||
name: "SushiSwap router - expect error due to complex ABI",
|
||||
tx: createMockTx(
|
||||
common.HexToAddress("0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506"), // SushiSwap Router
|
||||
[]byte{0x38, 0xed, 0x17, 0x39}, // swapExactTokensForTokens selector only
|
||||
),
|
||||
expectError: true, // Expected to fail due to insufficient ABI data
|
||||
expectSwap: false,
|
||||
},
|
||||
{
|
||||
name: "Unknown function selector",
|
||||
tx: createMockTx(
|
||||
common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Uniswap V3 Router
|
||||
[]byte{0xFF, 0xFF, 0xFF, 0xFF}, // Unknown selector
|
||||
),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := parser.ParseDEXInteraction(tt.tx)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
if tt.expectSwap {
|
||||
assert.NotEmpty(t, result.Protocol)
|
||||
assert.Equal(t, *tt.tx.To(), result.Router)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestL2MessageParser_IsSignificantSwap(t *testing.T) {
|
||||
logger := &logger.Logger{}
|
||||
parser := NewL2MessageParser(logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
interaction *DEXInteraction
|
||||
minAmountUSD float64
|
||||
expectSignificant bool
|
||||
}{
|
||||
{
|
||||
name: "Small swap - not significant",
|
||||
interaction: &DEXInteraction{
|
||||
AmountIn: big.NewInt(100000000000000000), // 0.1 ETH
|
||||
},
|
||||
minAmountUSD: 10.0,
|
||||
expectSignificant: false,
|
||||
},
|
||||
{
|
||||
name: "Large swap - significant",
|
||||
interaction: &DEXInteraction{
|
||||
AmountIn: big.NewInt(2000000000000000000), // 2 ETH
|
||||
},
|
||||
minAmountUSD: 10.0,
|
||||
expectSignificant: true,
|
||||
},
|
||||
{
|
||||
name: "Nil amount - not significant",
|
||||
interaction: &DEXInteraction{
|
||||
AmountIn: nil,
|
||||
},
|
||||
minAmountUSD: 10.0,
|
||||
expectSignificant: false,
|
||||
},
|
||||
{
|
||||
name: "Zero amount - not significant",
|
||||
interaction: &DEXInteraction{
|
||||
AmountIn: big.NewInt(0),
|
||||
},
|
||||
minAmountUSD: 10.0,
|
||||
expectSignificant: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parser.IsSignificantSwap(tt.interaction, tt.minAmountUSD)
|
||||
assert.Equal(t, tt.expectSignificant, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestL2MessageParser_ParseExactInputSingle(t *testing.T) {
|
||||
logger := &logger.Logger{}
|
||||
parser := NewL2MessageParser(logger)
|
||||
|
||||
// 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()
|
||||
copy(data[192-len(amountInBytes):192], amountInBytes)
|
||||
|
||||
interaction := &DEXInteraction{}
|
||||
result, err := parser.parseExactInputSingle(interaction, data)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, common.HexToAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), result.TokenIn)
|
||||
assert.Equal(t, common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"), result.TokenOut)
|
||||
assert.Equal(t, common.HexToAddress("0x742d35Cc6635C0532925a3b8D9C12CF345eEE40F"), result.Recipient)
|
||||
assert.Equal(t, uint64(1234567890), result.Deadline)
|
||||
// Note: AmountIn comparison might need adjustment based on how the data is packed
|
||||
}
|
||||
|
||||
func TestL2MessageParser_InitialSetup(t *testing.T) {
|
||||
logger := &logger.Logger{}
|
||||
parser := NewL2MessageParser(logger)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func BenchmarkL2MessageParser_ParseL2Message(b *testing.B) {
|
||||
logger := &logger.Logger{}
|
||||
parser := NewL2MessageParser(logger)
|
||||
|
||||
// Create test message data
|
||||
messageData := append([]byte{0x00, 0x00, 0x00, 0x03}, make([]byte, 100)...)
|
||||
messageNumber := big.NewInt(1)
|
||||
timestamp := uint64(1234567890)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := parser.ParseL2Message(messageData, messageNumber, timestamp)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkL2MessageParser_ParseDEXInteraction(b *testing.B) {
|
||||
logger := &logger.Logger{}
|
||||
parser := NewL2MessageParser(logger)
|
||||
|
||||
// Create mock transaction
|
||||
tx := types.NewTransaction(
|
||||
0,
|
||||
common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564"), // Uniswap V3 Router
|
||||
big.NewInt(0),
|
||||
21000,
|
||||
big.NewInt(1000000000),
|
||||
[]byte{0x41, 0x4b, 0xf3, 0x89}, // exactInputSingle selector
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := parser.ParseDEXInteraction(tx)
|
||||
if err != nil && err.Error() != "insufficient data for exactInputSingle" {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
102
pkg/arbitrum/types.go
Normal file
102
pkg/arbitrum/types.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package arbitrum
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
// L2MessageType represents different types of L2 messages
|
||||
type L2MessageType int
|
||||
|
||||
const (
|
||||
L2Unknown L2MessageType = iota
|
||||
L2Transaction
|
||||
L2BatchSubmission
|
||||
L2StateUpdate
|
||||
L2Withdrawal
|
||||
L2Deposit
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Parsed transaction data (if applicable)
|
||||
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
|
||||
}
|
||||
|
||||
// SequencerInfo contains sequencer-specific information
|
||||
type SequencerInfo struct {
|
||||
SequencerAddress common.Address
|
||||
Timestamp uint64
|
||||
BlockHash common.Hash
|
||||
PrevBlockHash common.Hash
|
||||
}
|
||||
|
||||
// BatchInfo contains batch transaction information
|
||||
type BatchInfo struct {
|
||||
BatchNumber *big.Int
|
||||
BatchRoot common.Hash
|
||||
TxCount uint64
|
||||
L1SubmissionTx common.Hash
|
||||
}
|
||||
|
||||
// L2TransactionReceipt extends the standard receipt with L2 data
|
||||
type L2TransactionReceipt struct {
|
||||
*types.Receipt
|
||||
L2BlockNumber uint64
|
||||
L2TxIndex uint64
|
||||
RetryableTicket *RetryableTicket
|
||||
GasUsedForL1 uint64
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
SlippageTolerance *big.Int
|
||||
MessageNumber *big.Int
|
||||
Timestamp uint64
|
||||
}
|
||||
Reference in New Issue
Block a user