package parsers import ( "context" "encoding/json" "fmt" "io" "math/big" "net/http" "net/url" "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/your-org/mev-bot/pkg/types" ) // ArbiscanValidator validates parsed swap events against Arbiscan API type ArbiscanValidator struct { apiKey string baseURL string httpClient *http.Client logger types.Logger swapLogger *SwapLogger } // ArbiscanLog represents a log entry from Arbiscan API type ArbiscanLog struct { Address string `json:"address"` Topics []string `json:"topics"` Data string `json:"data"` BlockNumber string `json:"blockNumber"` TimeStamp string `json:"timeStamp"` GasPrice string `json:"gasPrice"` GasUsed string `json:"gasUsed"` LogIndex string `json:"logIndex"` TransactionHash string `json:"transactionHash"` TransactionIndex string `json:"transactionIndex"` } // ArbiscanAPIResponse represents the API response from Arbiscan type ArbiscanAPIResponse struct { Status string `json:"status"` Message string `json:"message"` Result []ArbiscanLog `json:"result"` } // ValidationResult contains the result of validating a swap event type ValidationResult struct { IsValid bool `json:"is_valid"` SwapEvent *types.SwapEvent `json:"swap_event"` ArbiscanLog *ArbiscanLog `json:"arbiscan_log"` Discrepancies []string `json:"discrepancies,omitempty"` ValidatedAt time.Time `json:"validated_at"` } // NewArbiscanValidator creates a new Arbiscan validator func NewArbiscanValidator(apiKey string, logger types.Logger, swapLogger *SwapLogger) *ArbiscanValidator { return &ArbiscanValidator{ apiKey: apiKey, baseURL: "https://api.arbiscan.io/api", httpClient: &http.Client{ Timeout: 10 * time.Second, }, logger: logger, swapLogger: swapLogger, } } // ValidateSwap validates a parsed swap event against Arbiscan func (v *ArbiscanValidator) ValidateSwap(ctx context.Context, event *types.SwapEvent) (*ValidationResult, error) { // Fetch transaction logs from Arbiscan logs, err := v.getTransactionLogs(ctx, event.TxHash) if err != nil { return nil, fmt.Errorf("failed to fetch logs from Arbiscan: %w", err) } // Find the matching log by log index var matchingLog *ArbiscanLog for i := range logs { logIndex := parseHexToUint64(logs[i].LogIndex) if logIndex == uint64(event.LogIndex) { matchingLog = &logs[i] break } } if matchingLog == nil { return &ValidationResult{ IsValid: false, SwapEvent: event, Discrepancies: []string{fmt.Sprintf("log index %d not found in Arbiscan response", event.LogIndex)}, ValidatedAt: time.Now(), }, nil } // Compare parsed event with Arbiscan data discrepancies := v.compareSwapWithArbiscan(event, matchingLog) result := &ValidationResult{ IsValid: len(discrepancies) == 0, SwapEvent: event, ArbiscanLog: matchingLog, Discrepancies: discrepancies, ValidatedAt: time.Time(), } // Log validation result if len(discrepancies) > 0 { v.logger.Warn("swap validation discrepancies found", "txHash", event.TxHash.Hex(), "logIndex", event.LogIndex, "discrepancies", strings.Join(discrepancies, "; "), ) // Save discrepancy to swap logger for investigation if v.swapLogger != nil { rawData := common.Hex2Bytes(strings.TrimPrefix(matchingLog.Data, "0x")) if err := v.swapLogger.LogSwap(ctx, event, rawData, matchingLog.Topics, "arbiscan_validation"); err != nil { v.logger.Error("failed to log discrepancy", "error", err) } } } else { v.logger.Debug("swap validation successful", "txHash", event.TxHash.Hex(), "logIndex", event.LogIndex, ) } return result, nil } // getTransactionLogs fetches transaction logs from Arbiscan API func (v *ArbiscanValidator) getTransactionLogs(ctx context.Context, txHash common.Hash) ([]ArbiscanLog, error) { // Build API URL params := url.Values{} params.Set("module", "logs") params.Set("action", "getLogs") params.Set("txhash", txHash.Hex()) params.Set("apikey", v.apiKey) apiURL := fmt.Sprintf("%s?%s", v.baseURL, params.Encode()) // Create request req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } // Execute request resp, err := v.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute request: %w", err) } defer resp.Body.Close() // Read response body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } // Parse response var apiResp ArbiscanAPIResponse if err := json.Unmarshal(body, &apiResp); err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } if apiResp.Status != "1" { return nil, fmt.Errorf("Arbiscan API error: %s", apiResp.Message) } return apiResp.Result, nil } // compareSwapWithArbiscan compares a parsed swap event with Arbiscan log data func (v *ArbiscanValidator) compareSwapWithArbiscan(event *types.SwapEvent, log *ArbiscanLog) []string { var discrepancies []string // Compare pool address if !strings.EqualFold(event.PoolAddress.Hex(), log.Address) { discrepancies = append(discrepancies, fmt.Sprintf( "pool address mismatch: parsed=%s arbiscan=%s", event.PoolAddress.Hex(), log.Address, )) } // Compare transaction hash if !strings.EqualFold(event.TxHash.Hex(), log.TransactionHash) { discrepancies = append(discrepancies, fmt.Sprintf( "tx hash mismatch: parsed=%s arbiscan=%s", event.TxHash.Hex(), log.TransactionHash, )) } // Compare block number arbiscanBlockNumber := parseHexToUint64(log.BlockNumber) if event.BlockNumber != arbiscanBlockNumber { discrepancies = append(discrepancies, fmt.Sprintf( "block number mismatch: parsed=%d arbiscan=%d", event.BlockNumber, arbiscanBlockNumber, )) } // Compare log index arbiscanLogIndex := parseHexToUint64(log.LogIndex) if uint64(event.LogIndex) != arbiscanLogIndex { discrepancies = append(discrepancies, fmt.Sprintf( "log index mismatch: parsed=%d arbiscan=%d", event.LogIndex, arbiscanLogIndex, )) } // Compare topics (event signature and indexed parameters) if len(log.Topics) > 0 { expectedSignature := log.Topics[0] // UniswapV2 Swap signature if strings.EqualFold(expectedSignature, SwapEventSignature.Hex()) { // Validate sender and recipient from topics if len(log.Topics) >= 3 { arbiscanSender := common.HexToAddress(log.Topics[1]) arbiscanRecipient := common.HexToAddress(log.Topics[2]) if event.Sender != arbiscanSender { discrepancies = append(discrepancies, fmt.Sprintf( "sender mismatch: parsed=%s arbiscan=%s", event.Sender.Hex(), arbiscanSender.Hex(), )) } if event.Recipient != arbiscanRecipient { discrepancies = append(discrepancies, fmt.Sprintf( "recipient mismatch: parsed=%s arbiscan=%s", event.Recipient.Hex(), arbiscanRecipient.Hex(), )) } } } } // Note: We don't compare amounts directly here because they're scaled // and Arbiscan returns raw values. The caller should handle amount validation // by comparing raw log data if needed. return discrepancies } // ValidateSwapBatch validates multiple swap events from the same transaction func (v *ArbiscanValidator) ValidateSwapBatch(ctx context.Context, events []*types.SwapEvent) ([]*ValidationResult, error) { if len(events) == 0 { return nil, nil } // All events should have the same transaction hash txHash := events[0].TxHash // Fetch transaction logs once for all events logs, err := v.getTransactionLogs(ctx, txHash) if err != nil { return nil, fmt.Errorf("failed to fetch logs from Arbiscan: %w", err) } // Validate each event results := make([]*ValidationResult, 0, len(events)) for _, event := range events { // Find matching log var matchingLog *ArbiscanLog for i := range logs { logIndex := parseHexToUint64(logs[i].LogIndex) if logIndex == uint64(event.LogIndex) { matchingLog = &logs[i] break } } if matchingLog == nil { results = append(results, &ValidationResult{ IsValid: false, SwapEvent: event, Discrepancies: []string{fmt.Sprintf("log index %d not found", event.LogIndex)}, ValidatedAt: time.Now(), }) continue } // Compare discrepancies := v.compareSwapWithArbiscan(event, matchingLog) results = append(results, &ValidationResult{ IsValid: len(discrepancies) == 0, SwapEvent: event, ArbiscanLog: matchingLog, Discrepancies: discrepancies, ValidatedAt: time.Now(), }) } return results, nil } // parseHexToUint64 parses a hex string to uint64 func parseHexToUint64(hex string) uint64 { if strings.HasPrefix(hex, "0x") { hex = hex[2:] } n := new(big.Int) n.SetString(hex, 16) return n.Uint64() } // GetValidationStats returns validation statistics type ValidationStats struct { TotalValidated int `json:"total_validated"` Valid int `json:"valid"` Invalid int `json:"invalid"` ErrorRate float64 `json:"error_rate"` LastValidated time.Time `json:"last_validated"` } // Note: To use this validator in production: // 1. Create validator: validator := NewArbiscanValidator(apiKey, logger, swapLogger) // 2. After parsing a swap: result, err := validator.ValidateSwap(ctx, event) // 3. Check result.IsValid and log discrepancies // 4. Use for spot-checking in production or continuous validation in testing