Files
node/chains/rpc/debug_tool.go
T
zeekayandHanzo Dev 51a304804c chore: migrate luxd HTTP routes /ext -> /v1
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route
surface (one way, no backward compat). The node's baseURL is the source of
truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the
k8s/compose/gateway/explorer configs are updated to match.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-01 11:40:13 -07:00

303 lines
7.6 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rpc
import (
"bytes"
"github.com/go-json-experiment/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/log"
)
// DebugTool provides utilities for debugging RPC handler issues.
// Developer-friendly diagnostics with clear, actionable output.
type DebugTool struct {
baseURL string
client *http.Client
log log.Logger
}
// NewDebugTool creates a debug tool for RPC endpoint testing.
func NewDebugTool(baseURL string, logger log.Logger) *DebugTool {
if !strings.HasPrefix(baseURL, "http") {
baseURL = "http://" + baseURL
}
return &DebugTool{
baseURL: strings.TrimSuffix(baseURL, "/"),
client: &http.Client{
Timeout: 10 * time.Second,
},
log: logger,
}
}
// DiagnoseEndpoint performs comprehensive diagnostics on an RPC endpoint.
// Returns detailed information about what's working and what's not.
func (d *DebugTool) DiagnoseEndpoint(chainID ids.ID, alias string) *DiagnosticReport {
report := &DiagnosticReport{
ChainID: chainID,
Alias: alias,
Timestamp: time.Now(),
Tests: make([]TestResult, 0),
}
// Test different URL patterns
urlPatterns := d.getURLPatterns(chainID, alias)
for _, pattern := range urlPatterns {
result := d.testEndpoint(pattern)
report.Tests = append(report.Tests, result)
}
// Test common RPC methods
if bestURL := report.GetBestURL(); bestURL != "" {
report.RPCTests = d.testRPCMethods(bestURL)
}
return report
}
// getURLPatterns returns all possible URL patterns to test.
func (d *DebugTool) getURLPatterns(chainID ids.ID, alias string) []string {
patterns := []string{
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, chainID.String()),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, chainID.String()),
}
if alias != "" && alias != chainID.String() {
patterns = append(patterns,
fmt.Sprintf("%s/v1/bc/%s/rpc", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s/ws", d.baseURL, alias),
fmt.Sprintf("%s/v1/bc/%s", d.baseURL, alias),
)
}
// Also test without /v1 prefix (some setups might differ)
patterns = append(patterns,
fmt.Sprintf("%s/bc/%s/rpc", d.baseURL, chainID.String()),
)
return patterns
}
// testEndpoint tests a single endpoint URL.
func (d *DebugTool) testEndpoint(url string) TestResult {
result := TestResult{
URL: url,
Timestamp: time.Now(),
}
// First try a simple GET
resp, err := d.client.Get(url)
if err != nil {
result.Error = fmt.Sprintf("GET failed: %v", err)
result.Success = false
return result
}
defer resp.Body.Close()
result.StatusCode = resp.StatusCode
// Read body for debugging
bodyBytes, _ := io.ReadAll(resp.Body)
result.Response = string(bodyBytes)
// Now try a POST with JSON-RPC
rpcReq := map[string]interface{}{
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": []interface{}{},
"id": 1,
}
jsonBytes, _ := json.Marshal(rpcReq)
postResp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
result.Error = fmt.Sprintf("POST failed: %v", err)
result.Success = false
return result
}
defer postResp.Body.Close()
result.StatusCode = postResp.StatusCode
// Check if we got a valid JSON-RPC response
var rpcResp map[string]interface{}
if err := json.UnmarshalRead(postResp.Body, &rpcResp); err == nil {
if _, hasResult := rpcResp["result"]; hasResult {
result.Success = true
result.Response = fmt.Sprintf("Valid JSON-RPC response: %v", rpcResp["result"])
} else if errObj, hasError := rpcResp["error"]; hasError {
result.Success = false
result.Response = fmt.Sprintf("JSON-RPC error: %v", errObj)
}
}
return result
}
// testRPCMethods tests common RPC methods against an endpoint.
func (d *DebugTool) testRPCMethods(url string) []RPCTest {
methods := []string{
"web3_clientVersion",
"eth_blockNumber",
"eth_chainId",
"net_version",
"eth_syncing",
}
tests := make([]RPCTest, 0, len(methods))
for _, method := range methods {
test := RPCTest{
Method: method,
URL: url,
}
req := map[string]interface{}{
"jsonrpc": "2.0",
"method": method,
"params": []interface{}{},
"id": 1,
}
jsonBytes, _ := json.Marshal(req)
resp, err := d.client.Post(url, "application/json", bytes.NewReader(jsonBytes))
if err != nil {
test.Error = err.Error()
test.Success = false
} else {
defer resp.Body.Close()
var result map[string]interface{}
if err := json.UnmarshalRead(resp.Body, &result); err == nil {
if res, ok := result["result"]; ok {
test.Success = true
test.Result = fmt.Sprintf("%v", res)
} else if errObj, ok := result["error"]; ok {
test.Error = fmt.Sprintf("%v", errObj)
}
}
}
tests = append(tests, test)
}
return tests
}
// DiagnosticReport contains comprehensive endpoint diagnostic information.
type DiagnosticReport struct {
ChainID ids.ID
Alias string
Timestamp time.Time
Tests []TestResult
RPCTests []RPCTest
}
// TestResult represents a single endpoint test result.
type TestResult struct {
URL string
Success bool
StatusCode int
Response string
Error string
Timestamp time.Time
}
// RPCTest represents a test of a specific RPC method.
type RPCTest struct {
Method string
URL string
Success bool
Result string
Error string
}
// GetBestURL returns the first working URL from the tests.
func (r *DiagnosticReport) GetBestURL() string {
for _, test := range r.Tests {
if test.Success {
return test.URL
}
}
return ""
}
// String returns a human-readable report.
func (r *DiagnosticReport) String() string {
var b strings.Builder
b.WriteString(fmt.Sprintf("=== RPC Endpoint Diagnostic Report ===\n"))
b.WriteString(fmt.Sprintf("Chain ID: %s\n", r.ChainID))
if r.Alias != "" {
b.WriteString(fmt.Sprintf("Alias: %s\n", r.Alias))
}
b.WriteString(fmt.Sprintf("Timestamp: %s\n\n", r.Timestamp.Format(time.RFC3339)))
b.WriteString("=== Endpoint Tests ===\n")
for _, test := range r.Tests {
status := "❌ FAILED"
if test.Success {
status = "✅ SUCCESS"
}
b.WriteString(fmt.Sprintf("\n%s %s\n", status, test.URL))
b.WriteString(fmt.Sprintf(" Status Code: %d\n", test.StatusCode))
if test.Error != "" {
b.WriteString(fmt.Sprintf(" Error: %s\n", test.Error))
}
if test.Response != "" && len(test.Response) < 200 {
b.WriteString(fmt.Sprintf(" Response: %s\n", test.Response))
}
}
if len(r.RPCTests) > 0 {
b.WriteString("\n=== RPC Method Tests ===\n")
for _, test := range r.RPCTests {
status := "❌"
if test.Success {
status = "✅"
}
b.WriteString(fmt.Sprintf("%s %s: ", status, test.Method))
if test.Success {
b.WriteString(test.Result)
} else {
b.WriteString(test.Error)
}
b.WriteString("\n")
}
}
b.WriteString("\n=== Recommendations ===\n")
if bestURL := r.GetBestURL(); bestURL != "" {
b.WriteString(fmt.Sprintf("✅ Use this endpoint: %s\n", bestURL))
} else {
b.WriteString("❌ No working endpoints found. Check:\n")
b.WriteString(" 1. Is the node running?\n")
b.WriteString(" 2. Is the chain bootstrapped?\n")
b.WriteString(" 3. Are handlers properly registered?\n")
b.WriteString(" 4. Check node logs for handler registration errors\n")
b.WriteString(" 5. Try restarting the node\n")
}
return b.String()
}
// QuickDiagnose performs a quick endpoint check and prints results.
// Convenience function for CLI tools.
func QuickDiagnose(nodeURL string, chainID ids.ID, alias string) {
logger := log.NewNoOpLogger()
tool := NewDebugTool(nodeURL, logger)
report := tool.DiagnoseEndpoint(chainID, alias)
fmt.Println(report.String())
}