diff --git a/cmd/relayer/setup/setup.go b/cmd/relayer/setup/setup.go index e7bb786d9..1c0bb43fc 100644 --- a/cmd/relayer/setup/setup.go +++ b/cmd/relayer/setup/setup.go @@ -268,11 +268,16 @@ func getPreRunInfo(home string) (*consts.RollappData, *consts.HubData, string, e pterm.Error.Println("failed to retrieve rollapp rpc endpoint: ", err) return nil, nil, "", err } - raRpc = strings.TrimSuffix(raRpc, "/") + pterm.Info.Printf("validating rollapp rpc endpoint: %s\n", raRpc) + validatedRpc, err := sequencerutils.ValidateRollappRPCEndpoint(raID, raRpc) + if err != nil { + pterm.Error.Println("rollapp rpc endpoint validation failed:", err) + return nil, nil, "", err + } raData := consts.RollappData{ ID: raID, - RpcUrl: raRpc, + RpcUrl: validatedRpc, } return &raData, hd, kb, nil } diff --git a/utils/sequencer/rpc.go b/utils/sequencer/rpc.go new file mode 100644 index 000000000..ad783fea5 --- /dev/null +++ b/utils/sequencer/rpc.go @@ -0,0 +1,81 @@ +package sequencer + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" +) + +type tendermintStatusResponse struct { + Result struct { + NodeInfo struct { + Network string `json:"network"` + } `json:"node_info"` + } `json:"result"` +} + +func ValidateRollappRPCEndpoint(rollappID, endpoint string) (string, error) { + trimmed := strings.TrimSpace(endpoint) + if trimmed == "" { + return "", fmt.Errorf("rollapp rpc endpoint is empty") + } + + normalized := normalizeRpcScheme(trimmed) + normalized = strings.TrimRight(normalized, "/") + + statusURL := normalized + "/status" + client := &http.Client{ + Timeout: 10 * time.Second, + } + + resp, err := client.Get(statusURL) + if err != nil { + return "", fmt.Errorf("failed to reach rollapp rpc endpoint %s: %w", normalized, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf( + "rollapp rpc endpoint %s returned %s from /status", + normalized, + resp.Status, + ) + } + + var status tendermintStatusResponse + if err := json.NewDecoder(resp.Body).Decode(&status); err != nil { + return "", fmt.Errorf("failed to decode /status from %s: %w", statusURL, err) + } + + chainID := strings.TrimSpace(status.Result.NodeInfo.Network) + if chainID == "" { + return "", fmt.Errorf("rollapp rpc endpoint %s returned empty chain-id in /status response", normalized) + } + if chainID != rollappID { + return "", fmt.Errorf( + "rollapp rpc endpoint %s reports chain-id %s, expected %s", + normalized, + chainID, + rollappID, + ) + } + + return normalized, nil +} + +func normalizeRpcScheme(endpoint string) string { + switch { + case strings.HasPrefix(endpoint, "tcp://"): + return "http://" + strings.TrimPrefix(endpoint, "tcp://") + case strings.HasPrefix(endpoint, "ws://"): + return "http://" + strings.TrimPrefix(endpoint, "ws://") + case strings.HasPrefix(endpoint, "wss://"): + return "https://" + strings.TrimPrefix(endpoint, "wss://") + case strings.HasPrefix(endpoint, "http://"), strings.HasPrefix(endpoint, "https://"): + return endpoint + default: + return "https://" + endpoint + } +}